Jerry Dodge
Jerry Dodge

Reputation: 27286

How to show a universal data label for any type of chart series?

I'm using the TeeChart (TChart) which is distributed with Delphi 10.4 to display data. Different types of data are displayed in different types of chart series. More specifically, I support 4 different types of charts:

  1. Line Chart
  2. Pie Chart
  3. Vertical Bar Chart
  4. Horizontal Bar Chart

When it comes to the Line Chart and Vertical Bar Chart, I have successfully implemented a label which displays next to the cursor when the user hovers over any part of the chart. This label displays the detailed values associated with each possible series of those charts, and some charts might have 10 different series, for example, in which case all those values show in the label.

Screenshot - data label in line chart

Now I am trying to add the same support for the other 2 chart types (Pie and Horizontal Bar). However, I'm not having much luck. Let's take the Horizontal Bar Chart for example. When I apply the same code below, the data does not display correctly. More specifically, when I hover over a horizontal bar, it seems to depend on whether I'm hovering over the upper or lower half of the bar. Hovering over the upper half shows data for the previous item (or nothing at all if it's the first item in the chart), and hovering over the lower half of a bar shows the expected data, so it seems to be offset.

Screenshot - data label in horizontal bar chart

Further, I cannot make it work at all on pie charts. It shows a small little bit of the label, but the label is never populated, so it never successfully identifies the SeriesIndex.

Screenshot - data label in pie chart

The way this code works is that it expects to find a value for Tag on the TChart control..

  1. Handles the X axis (line and Vert Bar Chart)
  2. Handles the Y axis (Horz Bar Chart)

The charts have a label inside them named lblChartData (the TChart is also a container).

Then the charts have an OnMouseMove event handler:

procedure TDashChartBase.ChartMouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Integer);
begin
  ShowDataLabel(X, Y);
  lblChartData.Left:= X + 15;
  lblChartData.Top:= Y;
  Chart.Repaint;
end;

Then the code for ShowDataLabel:

procedure TDashChartBase.ShowDataLabel(const X, Y: Integer);
var
  S: String;
  SeriesIndex: Integer;
  V: Double;
  I: Integer;
  CursorX: Double;
  CursorY: Double;
  Serie: TChartSeries;

  function GetXValueIndex(const ASerie: TChartSeries; const AX: Double): Integer;
  var
    Idx: Integer;
  begin
    for Idx := 0 to ASerie.XValues.Count - 1 do begin
      if ASerie.XValue[Idx] >= AX then
        Break;
    end;
    Result := Idx - 1;
  end;

  function GetYValueIndex(const ASerie: TChartSeries; const AY: Double): Integer;
  var
    Idx: Integer;
  begin
    for Idx := 0 to ASerie.YValues.Count - 1 do begin
      if ASerie.YValue[Idx] >= AY then
        Break;
    end;
    Result := Idx - 1;
  end;

begin
  try
    Chart.Cursor:= crCross;
    if not (Chart.SeriesCount > 0) then begin
      //Chart does not have any series to display...
      lblChartData.Visible:= False;
    end else begin
      //Get cursor position of first series...
      Chart.Series[0].GetCursorValues(CursorX, CursorY);
      for I := 0 to Chart.SeriesCount-1 do begin
        Serie:= Chart.Series[I];
        //Identify index of item based on mouse position...
        case Chart.Tag of
          1:    SeriesIndex:= GetXValueIndex(Serie, CursorX);
          else  SeriesIndex:= GetYValueIndex(Serie, CursorY);
        end;
        if (SeriesIndex < 0) or (SeriesIndex > Serie.Count-1) then begin
          //Series index is out of range...
          lblChartData.Visible:= False;
          Break;
        end else begin
          if I = 0 then begin
            //Add the value mark text as the first line in label...
            S:= S + Serie.ValueMarkText[SeriesIndex] + sLineBreak;
            if (Trim(S) = '') then begin
              //No value exists for this plot point...
              lblChartData.Visible:= False;
              Break;
            end;
          end;
          //Add the value associated with this series to the label...
          case Chart.Tag of
            1:    V:= Double(Serie.ValuesList[1].Value[SeriesIndex]);
            else  V:= Double(Serie.ValuesList[0].Value[SeriesIndex]);
          end;
          S:= S + Serie.Title+': '+FormatFloat(Serie.ValueFormat, V) + sLineBreak;
        end;
      end;
      lblChartData.Caption:= S;
    end;
  except
    on E: Exception do begin
      lblChartData.Visible:= False;
    end;
  end;
end;

How can I make this work properly for the other series types?

(NOTE: Some irrelevant code was stripped out from above snippets)

Upvotes: 0

Views: 553

Answers (1)

BrakNicku
BrakNicku

Reputation: 5990

How to show a universal data label for any type of chart series?

Could be rewritten as:

How to find series and point's index under mouse cursor for any type of chart series?

You can easily get both using CalcClickedPart. Your mouse move handler can then be written as follows:

TDashChartBase.ChartMouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Integer);
var Part:TChartClickedPart;
begin
  lblChartData.Visible:=false;
  Chart.CalcClickedPart(Point(X,Y),Part);
  if (Part.Part=cpSeries) or (Part.Part=cpSeriesPointer) then
  begin
    //extract all needed data using Part.ASeries and Part.PointIndex
    lblChartData.Caption:=Part.ASeries.Name + ';' + Part.PointIndex.ToString;
    lblChartData.Left:= X + 15;
    lblChartData.Top:= Y;
    lblChartData.Visible:=true;
  end;
end;

Upvotes: 3

Related Questions