Volumepipe + bubble chart - segment area

TeeChart VCL for Borland/CodeGear/Embarcadero RAD Studio, Delphi and C++ Builder.
Post Reply
Luboslav
Newbie
Newbie
Posts: 3
Joined: Thu Aug 27, 2015 12:00 am

Volumepipe + bubble chart - segment area

Post by Luboslav » Mon Oct 05, 2015 9:12 am

Hello,

I have to combine volumepipe+bubble chart together. volumepipe = stages of projects, bubbles = project in current stage(radius = money costs).
*Is it possible? I was not successful in this task in design mode.bubbles were changing its position according many factors(radius, max y pos etc)

I tried to draw circles on canvas but i'm not able to determine area of one volumepipe segment.
How could I do that? something like this:
volumepipe_bubbles.PNG
volumepipe
volumepipe_bubbles.PNG (113.55 KiB) Viewed 6463 times
Thank you.
Lubos

Yeray
Site Admin
Site Admin
Posts: 9514
Joined: Tue Dec 05, 2006 12:00 am
Location: Girona, Catalonia
Contact:

Re: Volumepipe + bubble chart - segment area

Post by Yeray » Wed Oct 07, 2015 9:42 am

Hi Lubos,

You could draw the ellipses manually, but you'll have to calculate the polygons as TeeChart does internally.
Here it is a simple example:

Code: Select all

uses TeCanvas;

type TVolumePipeSeriesAccess=class(TVolumePipeSeries);

var Series1: TVolumePipeSeries;
    Series2: TPointSeries;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Chart1.View3D:=false;

  Series1:=Chart1.AddSeries(TVolumePipeSeries) as TVolumePipeSeries;
  Series1.FillSampleValues();

  Series2:=Chart1.AddSeries(TPointSeries) as TPointSeries;
  Series2.FillSampleValues();

  Chart1.OnAfterDraw:=Chart1AfterDraw;
end;

procedure TForm1.Chart1AfterDraw(Sender: TObject);

  function GetMaxMarkHeight : Integer;
  var t,
    tmpLines : Integer;
  begin
    result:=0;

    for t:=Series1.FirstValueIndex to Series1.LastValueIndex do
    Begin
      Series1.ParentChart.MultiLineTextWidth(TVolumePipeSeriesAccess(Series1).GetMarkText(t),tmpLines);

      if tmpLines>result then
         result:=tmpLines;
    end;

    result:=result*Chart1.Canvas.FontHeight;
  end;

var BoundingPoints: TFourPoints;
    i, xVal, lastX, yDisp: Integer;
    lastYDisp, overallWidth: TCoordinate;
    IDiff: Integer;
    poly  : TTrapeziumPoints;
    IPolyList: Array of TTrapeziumPoints;
    InnerRect: TRect;
    tmp: TCoordinate;
    tmpCone : Single;
    mid1, mid2: TPoint;
begin
  Chart1.Canvas.Brush.Style := bsClear;

  InnerRect:=Chart1.ChartRect;

  if Series1.Marks.Visible then
     InnerRect.Top:=InnerRect.Top+GetMaxMarkHeight;

  tmpCone:=(Series1.ConePercent div 2)*0.01;

  With InnerRect do
  Begin
    BoundingPoints[0]:= TeePoint(Left+2,Top+2);  //topleft

    tmp:={$IFNDEF FMX}Round{$ENDIF}(Top+(Bottom-Top) * tmpCone);

    BoundingPoints[1]:= TeePoint(Right-2, tmp); //topright

    tmp:={$IFNDEF FMX}Round{$ENDIF}(Bottom-((Bottom-Top) * tmpCone));

    BoundingPoints[2]:= TeePoint(Right-2, tmp); //bottomright
    BoundingPoints[3]:= TeePoint(Left+2,Bottom-2); //bottomleft
  end;

  IDiff:= BoundingPoints[1].Y-BoundingPoints[0].Y;
  overallWidth:=BoundingPoints[1].X-BoundingPoints[0].X;

  IPolyList:=nil;

  lastX:=BoundingPoints[0].X;
  lastYDisp:=0;

  if overallWidth<>0 then
  for i:=0 to Series1.Count-1 do
  if not Series1.IsNull(i) then
  Begin
    xVal:=TVolumePipeSeriesAccess(Series1).CalcSegment(i,Series1.YValues[i])+BoundingPoints[0].X; //add left displacement
    yDisp:=Round((((xVal-BoundingPoints[0].X)/overallWidth)*IDiff));

    poly[0]:=TeePoint(xVal,BoundingPoints[3].Y-yDisp); //right bottom
    poly[1]:=TeePoint(xVal,BoundingPoints[0].Y+yDisp); //right top
    poly[2]:=TeePoint(lastX,BoundingPoints[0].Y+lastYDisp); //left top
    poly[3]:=TeePoint(lastX,BoundingPoints[3].Y-lastYDisp); //left bottom

    SetLength(IPolyList,Length(IPolyList)+1);
    IPolyList[Length(IPolyList)-1]:=poly;

    lastYDisp:=yDisp;

    mid1:=TeePoint(poly[2].X + ((poly[1].X - poly[2].X) div 2), poly[2].Y + ((poly[1].Y - poly[2].Y) div 2));
    mid2:=TeePoint(poly[3].X + ((poly[0].X - poly[3].X) div 2), poly[3].Y + ((poly[0].Y - poly[3].Y) div 2));

    with Series1.ParentChart.Canvas do
    Begin
      AssignVisiblePen(Series1.Pen);
      Pen.Color:=Series1.ValueColor[i];
      Ellipse(Rect(poly[2].X, mid1.Y, poly[0].X, mid2.Y));
    end;

    lastX:=xVal;
  end;
end;
Best Regards,
ImageYeray Alonso
Development & Support
Steema Software
Av. Montilivi 33, 17003 Girona, Catalonia (SP)
Image Image Image Image Image Image Please read our Bug Fixing Policy

Luboslav
Newbie
Newbie
Posts: 3
Joined: Thu Aug 27, 2015 12:00 am

Re: Volumepipe + bubble chart - segment area

Post by Luboslav » Wed Oct 21, 2015 9:15 am

hi,

thank you very much for that example.
Sorry to say it but i'm not concerned on that ellipse.

I still cant figure out how to combine volumepipe and bubble chart together.
example of data :
6 project stages = 6 stages of volumepipe chart.
bubble chart data- could be in more active stages
Project1 -1,2
Project2 - 4
Project3 -3,5
Project4 -1,3
Project5 -4
Project6 -6
Project7 -1,3,4
Project8 -2
Project9 -2
Project10-2,3

I tried to add bubble serie on afterdraw event when I get the segment top left corner(from your example)- as addnullxy and then one bubble to the middle of segment. (x,y value form mid1,mid2 point).
I was not successfull. bubble was not in middle.

Please advice.
Thank you

Yeray
Site Admin
Site Admin
Posts: 9514
Joined: Tue Dec 05, 2006 12:00 am
Location: Girona, Catalonia
Contact:

Re: Volumepipe + bubble chart - segment area

Post by Yeray » Wed Oct 21, 2015 2:53 pm

Hello,

Could you please arrange a simple example project we can run as-is to reproduce the problem here?
Thanks in advance.
Best Regards,
ImageYeray Alonso
Development & Support
Steema Software
Av. Montilivi 33, 17003 Girona, Catalonia (SP)
Image Image Image Image Image Image Please read our Bug Fixing Policy

Luboslav
Newbie
Newbie
Posts: 3
Joined: Thu Aug 27, 2015 12:00 am

Re: Volumepipe + bubble chart - segment area

Post by Luboslav » Thu Oct 22, 2015 9:55 am

Code: Select all

type
  TVolumePipeSeriesAccess=class(TVolumePipeSeries);
  TArrayBounds= Array of TTrapeziumPoints;
  TpProj=^TrProj;
  TrProj=record
    name:string;
    activestage:string;
  end;
  TForm1 = class(TForm)
    Chart1: TChart;
    procedure FormCreate(Sender: TObject);
    procedure Chart1AfterDraw(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    pProj:TpProj;
    listProj:Tlist;
    bFilled:boolean;
    procedure fillprojdata(n:integer);
    function GetMaxMarkHeight : Integer;
    function Calculatebounds:TArrayBounds;
    procedure drawbubles(xArBounds:TArrayBounds);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  Series1: TVolumePipeSeries;
  Series2: TBubbleSeries;
implementation
{$R *.dfm}
function TForm1.GetMaxMarkHeight : Integer;
var t, tmpLines : Integer;
begin
  result:=0;

  for t:=Series1.FirstValueIndex to Series1.LastValueIndex do
  Begin
    Series1.ParentChart.MultiLineTextWidth(TVolumePipeSeriesAccess(Series1).GetMarkText(t),tmpLines);

    if tmpLines>result then
       result:=tmpLines;
  end;

  result:=result*Chart1.Canvas.FontHeight;
end;

procedure TForm1.fillprojdata(n:integer);
const sdata='01234';
var i,j,fpos,len:integer;
  sub:string;
begin
  randomize;
  listProj:=Tlist.Create;
  for I := 0 to n do begin
    New(pProj);
    fpos:=random(5);
    len:=random(length(sdata)-fpos)+1;
    pProj.name:='project '+inttostr(i+1);
    pProj.activestage:='';
    sub:=copy(sData,fpos,len);
    for j:=1 to length(sub) do begin
      pProj.activestage:=pProj.activestage+sub[j]+',';
    end;
    pProj.activestage:=copy(pProj.activestage,1,length(pProj.activestage)-1);
    listproj.Add(pProj);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Chart1.View3D:=false;
  Series1:=Chart1.AddSeries(TVolumePipeSeries) as TVolumePipeSeries;
  series1.ShowInLegend:=false;
  //five stages
  Series1.Add(100,'stage1');
  Series1.Add(100,'stage2');
  Series1.Add(100,'stage3');
  Series1.Add(100,'stage4');
  Series1.Add(100,'stage5');
  Chart1.OnAfterDraw:=Chart1AfterDraw;
  fillprojdata(19);
  bFilled:=false;
end;

procedure TForm1.FormDestroy(Sender: TObject);
var i:integer;
begin
  for i:=0 to listProj.Count-1 do begin
    pProj:=listproj[i];
    dispose(pProj);
  end;
end;

function TForm1.Calculatebounds:TArrayBounds;

var BoundingPoints: TFourPoints;
    i,j, xVal, lastX, yDisp: Integer;
    lastYDisp, overallWidth: TCoordinate;
    IDiff: Integer;
    poly  : TTrapeziumPoints;
    IPolyList: Array of TTrapeziumPoints;
    InnerRect: TRect;
    tmp: TCoordinate;
    tmpCone : Single;
    mid1, mid2: TPoint;
    circleCenter:Tpoint;
begin
  InnerRect:=Chart1.ChartRect;
  if Series1.Marks.Visible then
     InnerRect.Top:=InnerRect.Top+GetMaxMarkHeight;
  tmpCone:=(Series1.ConePercent div 2)*0.01;//koeficient zmensenia,stupanie pyramidy
  With InnerRect do
  Begin
    BoundingPoints[0]:= TeePoint(Left+2,Top+2);  //topleft

    tmp:=Round(Top+(Bottom-Top) * tmpCone);

    BoundingPoints[1]:= TeePoint(Right-2, tmp); //topright

    tmp:=Round(Bottom-((Bottom-Top) * tmpCone));

    BoundingPoints[2]:= TeePoint(Right-2, tmp); //bottomright
    BoundingPoints[3]:= TeePoint(Left+2,Bottom-2); //bottomleft
  end;
  IDiff:= BoundingPoints[1].Y-BoundingPoints[0].Y;
  overallWidth:=BoundingPoints[1].X-BoundingPoints[0].X;
  IPolyList:=nil;
  lastX:=BoundingPoints[0].X;
  lastYDisp:=0;

  if overallWidth<>0 then
  for i:=0 to Series1.Count-1 do
  if not Series1.IsNull(i) then
  Begin
    xVal:=TVolumePipeSeriesAccess(Series1).CalcSegment(i,Series1.YValues[i])+BoundingPoints[0].X;
    //add left displacement
    yDisp:=Round((((xVal-BoundingPoints[0].X)/overallWidth)*IDiff));

    poly[0]:=TeePoint(xVal,BoundingPoints[3].Y-yDisp); //right bottom
    poly[1]:=TeePoint(xVal,BoundingPoints[0].Y+yDisp); //right top
    poly[2]:=TeePoint(lastX,BoundingPoints[0].Y+lastYDisp); //left top
    poly[3]:=TeePoint(lastX,BoundingPoints[3].Y-lastYDisp); //left bottom

    SetLength(IPolyList,Length(IPolyList)+1);
    IPolyList[Length(IPolyList)-1]:=poly;

    lastYDisp:=yDisp;

    lastX:=xVal;
  end;
  result := TArrayBounds(IPolyList);
end;

procedure TForm1.Chart1AfterDraw(Sender: TObject);
begin
  if not bFilled then
   drawbubles( Calculatebounds);
end;

procedure TForm1.drawbubles(xArBounds: TArrayBounds);
 var   poly  : TTrapeziumPoints;
   newp, mid1, mid2: TPoint;
  x,y,i,nProj: Integer;
  seriesX: TBubbleSeries;
  slist:Tstringlist;
begin
  randomize;
   // poly[0] //right top
   // poly[1] //right bottom
   // poly[2]//left bottom
   // poly[3]//left top
  for i:= Chart1.SeriesList.Count-1 downto 0 do begin
    if chart1.Series[i] is TBubbleSeries then
       chart1.Series[i].Free;
  end;
  slist:=TStringList.Create;
  slist.StrictDelimiter:=true;
  slist.Delimiter:=',';
  for nproj := 0 to listproj.Count-1 do begin
    pProj:=listproj[nproj];
    seriesX:=TBubbleSeries.Create(Chart1);
    seriesX.ParentChart :=  Chart1;
    seriesX.title := pProj^.name;
    seriesX.ColorEachPoint:=false;
    slist.Clear;
    slist.DelimitedText:=pproj^.activestage;
    for i:=0 to slist.Count-1 do begin
      poly:= xArBounds[strToInt(slist[i])];
      y:=poly[1].y+Random(poly[0].y-poly[1].y);
      x:=random(poly[1].X-poly[2].x)+poly[2].X;
      seriesX.AddBubble(x,y ,random(10));
      //mid1 := TeePoint(poly[2].X + ((poly[1].X - poly[2].X) div 2), poly[2].Y + ((poly[1].Y - poly[2].Y) div 2));
      //mid2 := TeePoint(poly[3].X + ((poly[0].X - poly[3].X) div 2), poly[3].Y + ((poly[0].Y - poly[3].Y) div 2));
      //newp:=  Teepoint(mid2.X,(mid1.y-mid2.y)div 2);
      //seriesX.AddBubble(newp.x,newp.y ,5);
      //seriesX.AddNullXY(poly[2].x,poly[2].Y);
    end;
  end;
  slist.Free;
  bFilled:=true;
end;

end.

Yeray
Site Admin
Site Admin
Posts: 9514
Joined: Tue Dec 05, 2006 12:00 am
Location: Girona, Catalonia
Contact:

Re: Volumepipe + bubble chart - segment area

Post by Yeray » Fri Oct 23, 2015 10:58 am

Hello,

I see in your code you are populating a TBubbleSeries at OnAfterDraw event. I understand you need the chart to be drawn to calculate the positions of the bubbles relative to the TVolumePipeSeries segments, but it would be more appropriate to force a chart repaint (Chart1.Draw) at OnCreate and calculate the positions after it.
The OnAfterDraw event is appropriate to manually draw shapes and lines (Chart1.Canvas.Line, Chart1.Canvas.Ellipse, etc) but not for populating series in general.

On the other hand, note the polygons you are calculating are using pixel positions while adding values to a series expects to take axis values. However, since the TVolumePipeSeries doesn't use the regular axes, the usual functions to convert from pixels to values can't be used to populate the TBubbleSeries.

I would try to follow this:
- Enhance your TrProj structure to also store color, x, y and radius.
- Create the TVolumePipeSeries at OnCreate, as you already do.
- Create as many TBubbleSeries (with title and color but without points) as TrProj you'll have later. This is just to populate the legend.
- Force a repaint (Chart1.Draw). Now the chart has the final size (note the legend shrinks the chart).
- Calculate the polygons for the segments.
- Populate your TrProj structure with the desired data, still at OnCreate.
- At OnAfterDraw, loop for your TrProj structure as you do and manually draw your ellipses. Ie:

Code: Select all

      Chart1.Canvas.Brush.Color:=pProj.color;
      Chart1.Canvas.Ellipse(Rect(pProj.x-pProj.r, pProj.y-pProj.r, pProj.x+pProj.r, pProj.y+pProj.r));
Note that this way you are storing absolute pixel positions to your pProj structure and, if you resize the chart the TVolumePipeSeries will be resized with it but not your ellipses. So if your chart is resized you should recalculate the polygons for the TVolumePipeSeries segments, repopulate your TrProj structure and repaint.
Best Regards,
ImageYeray Alonso
Development & Support
Steema Software
Av. Montilivi 33, 17003 Girona, Catalonia (SP)
Image Image Image Image Image Image Please read our Bug Fixing Policy

Post Reply