Resizable panes again lazarus / 55-TAChart ---------------------------------------------------------------------- fpcoder · 2022-02-18 01:41 Based on the example with resizable panes we had earlier, I wanted to add the panes to the chart so they always appear below existing panes. https://forum.lazarus.freepascal.org/index.php/topic,45989.0.html Solution below rearranges existing axises and dividers when a new pane is added. It works until I start resizing the panes by moving the divider. Problem is that since the order of appearance is now swapped upside down, the code you wrote in DataPointDragToolDrag needs to be rewritten. I tried all day but didn't manage to swap it. Comes out a little wrong whatever I do. Plus when adding a pane, it gets a few pixels too much height as seen on attached picture. Here's the code I use for rearranging existing panes when adding new ones: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TChartPanes.ConfigureAxisTransformations(var AChartPane: TChartPane);var  i:Integer;begin  if Self.Count < 1 then  begin    aChartPane.AxisTransform.MinValue:= HALF_GAP;    aChartPane.AxisTransform.MaxValue:= 1.85;  end else  begin    { Shift all panes one step up }    { to make room for new pane on the bottom }    for i:= 0 to Count -1 do    begin      Items[i].AxisTransform.MinValue:=        Items[i].AxisTransform.MaxValue + 0.30;       Items[i].AxisTransform.MaxValue:=        Items[i].AxisTransform.MaxValue + 1.70;       If Assigned(Items[i].Divider) then        Items[i].Divider.Position:=        Items[i].AxisTransform.MaxValue + HALF_GAP;    end;     aChartPane.AxisTransform.MinValue:= HALF_GAP;    aChartPane.AxisTransform.MaxValue:= 1.85;     //Create Pane Divider and set Divider.Position    aChartPane.Divider:= TConstantLine.Create(FChart);    aChartPane.Divider.Position:=      Items[Count -1].AxisTransform.MinValue - HALF_GAP;     FChart.AddSeries(aChartPane.Divider);  end;end;  Lots of commenting while trying to rewrite the DragToolDrag procedure. If any comments are wrong, please tell. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TChartPanes.DataPointDragToolDrag(ASender: TDataPointDragTool;  var AGraphPoint: TDoublePoint);const  MIN_SIZE = 0.5;  // Min size of a pane, avoid div by zerovar  pos, lowerPos, upperPos: Double;  ser: TConstantLine;  ex: TDoubleRect;  PaneIndex: Integer;  pane_above, pane_below: TChartPane;begin  if not (TDataPointTool(ASender).Series is TConstantLine) then exit;   ser := TConstantLine(TDataPointDragTool(ASender).Series);  ex := ser.ParentChart.GetFullExtent;   //Panes that are added later will always  //appear below panes that were added earlier.  //TChartPanes.ConfigureAxisTransformations arranges this.   //PaneIndex will always be at least 1 because the  //first pane, (PaneIndex 0) never has a divider  PaneIndex := GetPaneIndexOfDivider(ser);   //pane_above will always be 1 count less than  //pane_below because it is added before pane_below  pane_above := GetChartPane(PaneIndex -1);   //pane_below is the pane that the divider belongs to  pane_below := GetChartPane(PaneIndex);   //There is nothing to divide with only one pane.  //So there exists no dividers until more than one  //pane is created, PaneIndex will thus always be  //at least 1. Also, Count-1 will be at least 1  //because this procedure never runs until there  //are at least two panes.   //Calculates upper position of pane_below  if PaneIndex < Count-1 then    upperPos := GetChartPane(PaneIndex +1).Divider.Position            - MIN_SIZE - HALF_GAP  else    upperPos := ex.b.y - MIN_SIZE - HALF_GAP;   //Calculates lower position of pane_above  if PaneIndex = 1 then    lowerPos := ex.a.y + MIN_SIZE + HALF_GAP  else    lowerPos := GetChartPane(PaneIndex -1).Divider.Position              + MIN_SIZE + HALF_GAP;   //Ensures that the Y-axises of both pane_above and  //pane_below are within their designated chart areas  AGraphPoint.Y := EnsureRange(AGraphPoint.Y, lowerPos, upperPos);   //Adjusts axises of both panes using the values collected  //from calculations above  pane_above.AxisTransform.MaxValue := AGraphPoint.Y - HALF_GAP;  pane_below.AxisTransform.MinValue := AGraphPoint.Y + HALF_GAP;   //Adjust divider position  ser.Position := AGraphPoint.Y;end;  Small demo attached. ---------------------------------------------------------------------- wp · 2022-02-18 14:57 Nice example. Rather than rearranging the existing series and their transformations when a new pane is added at the bottom I thought it would be easier to introduce a PaneIndex which can grow from the center value (0) in both directions. This automatically allows to add panes above and below the existing stack without having to rearrange the Transformation's MinValue and Maxvalue. So, the first pane gets the PaneIndex 0, and when I add another pane on top of it the new pane has the PaneIndex 1 etc. When I add another pane below the center pane its Pane Index will be -1, etc. Of course, do not confuse the PaneIndex with the normal index into the panes FPList running between 0 and Count-1. The draggable divider lines sit at the edges of the panes. The pane with PageIndex 0 has no divider. All other panes contain one divider. The panes above it (PaneIndex > 0) have their dividers at their bottom edge, the panes below it (PaneIndex < 0) have their dividers at their top edge. Panes with PaneIndex > 0 have the transformation.MinValue a HALF_GAP higher than the divider position, the Transformation.MaxValue is a HALF_GAP lower than the upper edge of the pane. Panes with PaneIndex < 0 thave the transformation MaxValue lower than the divider position by a HALF_GAP, and the MinValue is higher than the lower end of the pane by HALF_GAP. Please have a look at the attached modified version of you sample. ---------------------------------------------------------------------- fpcoder · 2022-02-18 18:18 The PaneIndex is a great idea. I actually hoped you would come up with something better than my rearranging of axises. I noticed though, that left axises now overlap with a HALF_GAP or two in height. (Axises overwrite each other slightly in the adjacent area, see pic.) I tried to modify the code in ConfigureAxisTransformation, but had no immediate success. I'll go over the new code now to understand how everything works. ---------------------------------------------------------------------- wp · 2022-02-18 18:30 Increase the HALF_GAP to 0.1. With such a value it looks better. Alternatively you can reactivate the line APane.Axis.Marks.AtDataOnly := true in TChartPanes.AssignAxisToPane. To avoid the jitter of the left axis when the axis label values change due to pane resizing, you can set APane.Axis.LabelSize to a reasonably large value (like: 50) in TChartPanes.AssignAxisToPane (it depends on the length of the labels and the presence of an axis title). To avoid jumping of the axis when panes are added for the first time you should use the same value in the object inspector for the chart's LeftAxis. ---------------------------------------------------------------------- fpcoder · 2022-02-18 19:03 Increasing HALF_GAP to 0.1 took care of the overlapping. Thanks! Although, I see now, the first added pane is now initially larger than panes added later. Also, is it possible to remove the jitter at the top of the chart when resizing a topmost pane? ---------------------------------------------------------------------- wp · 2022-02-18 21:20 Quote from: kapibara on February 18, 2022, 08:03:14 pmAlthough, I see now, the first added pane is now initially larger than panes added later. Ah, we don't set the Transformation.MinValue and MaxValue for the center pane. Modify the initial instruction in TChartPanes.ConfigureAxisTransformation in which the procedure is exited if AChartPane.PaneIndex is 0 as follows: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TChartPanes.ConfigureAxisTransformation(var AChartPane: TChartPane);var  neighborPane: TChartPane;begin  if AChartPane.PaneIndex = 0 then  begin    AChartPane.AxisTransform.MinValue := HALF_GAP;                      // <------ added    AChartPane.AxisTransform.MaxValue := DEFAULT_PANE_SIZE - HALF_GAP;  // <---- added    exit;  end;   ... Quote from: kapibara on February 18, 2022, 08:03:14 pmAlso, is it possible to remove the jitter at the top of the chart when resizing a topmost pane? Add a top axis; the extra space due to the labels prevents the uppermost y label to reach over the upper chart edge. If you don't want to see the labels at the top you should set TopAxis.Marks.Style = smsNone and TopAxis.TickLength = 0. Then set the LabelSize to some appropriate value. In my tests, LabelSize as small as 4 is enough for the standard y axis font and with the standard Chart.Margins.Top ---------------------------------------------------------------------- fpcoder · 2022-02-19 06:26 That works! I'll go on adding code for removing panes and properly free things and so on. Later it might be useful to have captions on the panes, showing text. Do you know if this would be possible to achieve?  I saw there is a unit called TADrawUtils with TextOut functions etc. ---------------------------------------------------------------------- wp · 2022-02-19 11:10 Rethinking this task I noticed that it is not necessary to introduce a dedicated PaneIndex. The only important point is that panes are added to the top of the pane stack by calling "ChartPanes.Add(newPane)" and to the bottom of the stack by calling "ChartPanes.Insert(0,newPane)". Now the divider line can always stay at the bottom of each pane, except for the very first pane which does not have a divider at all. This simplifies some of the code. See new version in attachment. ---------------------------------------------------------------------- fpcoder · 2022-02-19 17:00 That runs fine. In ConfigureAxisTransformation, there was a duplication of code. I removed one of the lines. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   // Set up the divider line...  dividerPane.Divider := TConstantline.Create(FChart); ---------------------------------------------------------------------- wp · 2022-02-19 17:37 Quote from: kapibara on February 19, 2022, 07:26:24 amLater it might be useful to have captions on the panes, showing text. Do you know if this would be possible to achieve?  I saw there is a unit called TADrawUtils with TextOut functions etc. TAChart does not support any freely-placeable text labels at the moment. Therefore you must do this yourself. First of all you must find the correct place to hook in, probably the OnAfterDraw event of the chart. This event has the drawer which does the painting as a parameter. And this drawer calls its TextOut method which returns a TChartTextOut class. Note, however, that TChartTextOut is a bit unusual in that its methods must be chained. So, for example to draw the text "Pane #1" at the location 100, 200 (in image coordinates) you call Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   Drawer.TextOut.Text('Panel #1').Pos(100, 200).Done;The final ".Done" executes the drawing. ---------------------------------------------------------------------- fpcoder · 2022-03-07 20:17 wp, I need some help again. Here is a class TChartBox I came up with to make it easier to work with panes. It uses the class TChartPane from before. The constructor of TChartBox takes a wincontrol and shows a chart on it. Panes are added seamlessly to the chart when adding a series. How to use TChartBox to add a series in a new pane: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i ChartBox.AddSeries(ASeries: TChartSeries; NewPane: Boolean; PaneIndex: Integer = -1): TChartPane; //First parameter: The series to add to the pane//Second parameter: Use new or existing pane for that series//Third parameter: Append pane or insert it at Index. By default it appends (-1)  Example of appending a pane with new series. (Pane is added bottom- most): Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i ChartBox.AddSeries(myseries, True, -1); Inserting series in new top-most pane: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i ChartBox.AddSeries(myseries, True, 0); This seems like a convenient way to add panes. After all, it is the series that are most important. The panes are just where to show them, so maybe no need to add them in a separate step. What do you think? I messed up your code in procedure DataPointDragToolDrag. The resizing is now a bit out of sync, I think because the top-most pane is stored at index zero instead of last in the panes list. If you could have a look? Also, when pressing "Add pane" for the third time the divider between first and second pane is removed. I'm going a bit nutters over why that happens. "Insert pane" works fine though. ("Insert Between" is not working yet) I've tried to comment the changes in code since last time. Since its not possible to know from the start what Min/Max values the top-most chart will have, I ended up keeping the top-most pane at index zero in the pane list. Maybe I'm missing advantages with keeping it at the end?   { Rules for panes:     The pane at index 0 is always top-most.     New panes can be inserted before it, or appended after it.     Panes can be inserted between other panes.     Four cases for configuring Panes:     1) If there is only one pane:        MaxValue := DEFAULT_PANE_SIZE - HALF_GAP        MinValue := HALF_GAP;        Dont create any Divider, not needed.     2) Pane was appended to the PaneList, and at least one pane        already existed. New pane will be shown at the bottom of chart:        MaxValue := DEFAULT_PANE_SIZE - HALF_GAP        MinValue := HALF_GAP;        Add Divider        Shift Min/Max of all earlier Panes upwards by DEFAULT_PANE_SIZE     3) Inserted FIRST in PaneList. (Shown topmost on chart)        (Shown on chart above previous topmost pane)        MaxValue := DEFAULT_PANE_SIZE - HALF_GAP        MinValue := HALF_GAP;        Add Divider        No need to shift old panes, the new one has higher Min/Max.     4) Inserted BETWEEN two existing Panes using PaneIndex:        (Shown on chart above Pane with PaneIndex)        Assign Min value from lower neighbour:        MinValue := Panes[Count -1].Min        MaxValue := DEFAULT_PANE_SIZE - HALF_GAP;        Add Divider        Shift all panes above PaneIndex upwards by DEFAULT_PANE_SIZE   } ---------------------------------------------------------------------- wp · 2022-03-07 22:22 Nice idea. That the pane index increases from top down to the bottom is a bit counter-intuitive to me because the related Min/MaxValues grow from bottom to top. This makes thinking unnecessarily complicated. It certainly is the reason why "Insert" is not working, or the DragTool is crashing.  Can't you just exchange the List.Add/List.Insert calls and then rework the logics? I mean: - Add a pane at the top: --> List.Add(pane) - Add a pane at the bottom: --> List.Insert(0, pane) This way, when you move up from the bottom, you start with pane #0 MinValue, move up to pane#0 Maxvalue, switch over to pane #1 Minvalue, then pane #1 maxValue, etc. A very linear way of thinking. ---------------------------------------------------------------------- fpcoder · 2022-03-07 22:37 Thats worth a try, I'll rework it and we'll see. ---------------------------------------------------------------------- fpcoder · 2022-06-13 04:39 Hi wp, I add panes your way now and it really made things easier. :-) The code is re-arranged into two classes, TChartBox and TChartPane. Now I'm into deleting panes and got some questions: If a ChartPane is deleted, the remaining ones are aligned automatically. How come this works without calling ConfigureAxisTransformation? Although it works, it is dependant on that Panes that are added from bottom up must be removed in reverse order, top and down (Index=Count -1). And the opposite for panes added from top and down (Index= 0). Otherwise remaining panes don't align after deletes. This may also pose a special problem if some panes are added from the top but others are added from the bottom? How would you deal with deletes here? Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i ChartBox.RemovePane(0); Also, after removing a pane, there appears a dangling divider one millimeter from the bottom of the chart. Trying to drag it results in index out of bounds. But the Divider is a member of TChartPane and is free'd in the destructor of TChartPane. So at least one divider seems to hang on somewhere. But where.. :-) Removing the last pane also removes the dangling Divider though. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i destructor TChartPane.Destroy; ..if Assigned(FDivider) thenbegin  FChart.RemoveSeries(FDivider);  FreeAndNil(FDivider);end;..  If you run the program, I suggest you use the button "Add a LineSeries in a new pane at the bottom" to add panes with. Then the remaining panes will align themselves after deletes as I wrote above. Any comments and improvements highly welcome. ---------------------------------------------------------------------- fpcoder · 2022-06-13 07:41 It would be really cool if it was possible to click a pane and delete it from, for example, a popup menu. ---------------------------------------------------------------------- fpcoder · 2022-06-13 08:42 I found the "dangling" divider happens only for panes added at the bottom, not top. This divider appears to belong to the neighbor pane, which doesn't need divider anymore when the pane below is deleted. Hmm, how to best delete a divider that belongs to another pane than the deleted one? ---------------------------------------------------------------------- fpcoder · 2022-06-14 05:28 Turned out the Divider was not dangling. It just belonged to the pane above and seemed to be left over since the neighboring pane below was deleted. I solved it in the new version by introducing a RemoveDivider procedure in TChartBox. So thats some progress, but here's a situation I'd like to handle next: Pane 1 is added topmost Pane 2 is added bottommost Pane 3 is added bottommost Pane 4 is added topmost But remove Pane 4 and Pane 3, and the party is over and panes misalign. If possible, it should be solved in a way that allows for a pane between other panes to be selected and deleted. ---------------------------------------------------------------------- fpcoder · 2022-06-15 04:40 I think maybe I get it now, how it should work. Except how to get a pane by clicking the chart. ---------------------------------------------------------------------- wp · 2022-06-15 08:46 Quote from: kapibara on June 15, 2022, 06:40:57 amI think maybe I get it now, how it should work. Except how to get a pane by clicking the chart. Good that you found it yourself. For finding the clicked pane, you could add a TUserDefinedTool to the ChartBox and implement a handler for the OnAfterMouseDown event. Here you get the X,Y coordinates of the clicked point in image coordinates. Convert it to graph coordinates (Chart.ImageToGraph) which is the unit system in which the pane borders are defined in the MinValue and MaxValue of the axis tranform. Then, iterate over all panes and check whether the clicked graph point is within the MinValue-to-MaxValue range of the pane's axis transform. ---------------------------------------------------------------------- fpcoder · 2022-06-20 01:42 Well, it's getting closer, but still the problem that panes get incorrect size after deleting one of them. I don't understand how to fix it. I wrote the procedure AdjustPanesAfterDelete to see what happens if to keep the default min/max values for pane 0 and up. The adjustment is shown in the legend after deleting a pane. You can just comment it out. ---------------------------------------------------------------------- fpcoder · 2022-06-20 20:15 I changed TChartPane's destructor how to remove transformations from a pane. Hope its OK to remove them like this? Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   destructor TChartPane.Destroy;  ..    AxisTransform.Free;    FAxis.Transformations.Free;    FAxis.Transformations:= NIL;     FAxis.Free;  ..  end; ---------------------------------------------------------------------- wp · 2022-06-20 21:33 Yes. Just added this to the wiki: https://wiki.lazarus.freepascal.org/TACha rt_Runtime_FAQ#How_to_remove_an_axis_transformation.3F ---------------------------------------------------------------------- fpcoder · 2022-06-21 22:28 Thanks. It seems that my problem with the axes is because when a pane is deleted an axis is removed, and then the axis indexes of the remaining panes dont match the AxisIndexY of the LineSeries. What to do? I added some panels to view whats going on inside. Edit: If the panes are added at the bottom, they can be deleted and axes still works. Only adding at the top causes a problem. ---------------------------------------------------------------------- wp · 2022-06-22 05:22 Quote from: kapibara on June 22, 2022, 12:28:04 amIt seems that my problem with the axes is because when a pane is deleted an axis is removed, and then the axis indexes of the remaining panes dont match the AxisIndexY of the LineSeries. What to do? Good catch. The AxisIndex seems to be a bad concept for identification of an axis in such an application... Anyway, so when you delete an axis why don't you iterate over all series and decrement their AxisIndexY if it is greater than the Index of the deleted axis? In fact, you already have it in TChartBox.RemovePane, but commented it out. Just store the axisindex before deleting the pane (--> pane_axisindex), remove the comment slashes and add an "if AxisIndexY > pane_axisindex": Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TChartBox.RemovePane(APaneIndex: Integer);var  ...  pane_axisindex: Integer;  begin  ...  pane:= TChartPane(FPaneList[APaneIndex]);  pane_axisindex := pane.Axis.Index;                             // <--- new  //When deleting the bottom-most pane (has no divider),  //the divider of its neighbor must be removed instead.  if (APaneIndex = 0) and (PaneCount > 1) then  begin    divider_pane:= TChartPane(FPaneList[APaneIndex +1]);//    divider_pane.axis.Visible:=false;    divider_pane.RemoveDivider;    pane.Free;  //Also removes series from chart    FPaneList.Delete(APaneIndex);    //AdjustPanesAfterDelete;    for i:=0 to FChart.SeriesCount -1 do begin                  // <-- uncommented      if FChart.Series[i] is TLineSeries then      with FChart.Series[i] as TLineSeries do        if AxisIndexY > pane_axisIndex then                  // <--- new          AxisIndexY:= AxisIndexY -1;    end;  end ---------------------------------------------------------------------- fpcoder · 2022-06-23 03:53 That addition made it possible to remove panes both from the top and from the bottom with axes working as they should. Super!