Recent

Author Topic: Stacked bar series  (Read 12042 times)

wp

  • Hero Member
  • *****
  • Posts: 13433
Stacked bar series
« on: May 15, 2012, 11:44:05 pm »
Unlike my other projects where I use line series I wanted to apply stacked bar series now. Unfortunately, this seems to be much more complex than using multiple line series. Using the multidemo I was able to figure out the basics. So, this posting will mainly be a step-by-step instruction on how to create a stacked bar chart. There will be a little question at the end, though.

- In Delphi, each bar of the stack is a separate series. In TAChart, there is only a single bar series, the data for the bars are stored in the ylist of the series' chartsource.
- It is advantageous to use a separate ChartSource, usually a ListChartSource. So, don't add the data to the series directly, but to the ListChartSource instead. The y values of the bottom-most bar are added via ListChartSource.Add, the y values of the upper bars are added by the SetYList method. Don't forget to set the YCount property accordingly. Add also a label for the x axis marks to the ListChartSource.
- Assign the ListChartSource to the Marks property of the chart's bottom axis, and set Marks.Style to smsLabel.
- Usually, you assign the ListChartSource to the Source property of the BarSeries. Or use an intermediate CalculatedChartSource if you want to have all stacks expanded to the same height for better comparison of the bars contributions (set CalculatedChartSource.Percentage = true); set its Origin to the ListchartSource and used it as Source of the BarSeries.
- Finally, a ChartStyles component has to be added to the form, it must be linked to the Style property of the bar series. For each bar of the stack add a style and specify, for example, its color there.
- If you want to display marks above each bar set the series' Marks.Style accordingly. It is important to set Marks.YIndex to -1 in order to get marks at all bars.

Is this correct? Is something missing?

The only thing that I could not figure out is how to get a legend item for each bar of the stack. How could I get this done?
« Last Edit: May 17, 2012, 12:43:55 pm by wp »

Ask

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 687
Re: Stacked bar series
« Reply #1 on: May 17, 2012, 06:00:47 pm »
Quote
Is this correct?
Yes. Perhaps you should also set MarkPositions=lmpInside

Quote
The only thing that I could not figure out is how to get a legend item for each bar of the stack. How could I get this done?
Not implemented yet. The whole stacked series subsystem is currently somewhat theoretical,
because you are the first known user of this feature ;-)

Ask

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 687
Re: Stacked bar series
« Reply #2 on: June 05, 2012, 10:51:30 am »
Quote
Not implemented yet.

Added lmStyle multiplicity in r37526-37529.
See "Stacked" page of the multi demo for an example.

JD

  • Hero Member
  • *****
  • Posts: 1910
Re: Stacked bar series
« Reply #3 on: June 05, 2012, 12:01:23 pm »
I think we all owe wp and Ask a big thank you because I can see how much TAChart has evolved/improved because of the collaboration of the two of you.
« Last Edit: June 05, 2012, 11:58:57 pm by JD »
Linux Mint - Lazarus 4.0/FPC 3.2.2,
Windows - Lazarus 4.0/FPC 3.2.2

mORMot 2, PostgreSQL & MariaDB.

wp

  • Hero Member
  • *****
  • Posts: 13433
Re: Stacked bar series
« Reply #4 on: June 05, 2012, 08:45:49 pm »
@Ask: Thank you. Perfect, as usual.

@JD: You're welcome, nice to get positive feedback. 99% of the credit go to Ask.

wp

  • Hero Member
  • *****
  • Posts: 13433
Re: Stacked bar series
« Reply #5 on: June 07, 2012, 04:49:00 pm »
A solution leads to new problems...

Since for stacked bar series it is quite usual to have multiple legend entries the TChartListbox is not working any more - so far, it assumes that there is only a single legend entry per series, and it crashes when assigned to a chart with stacked bars. I modified TChartListbox.CreateLegendItems to create multiple listbox items in the following way:

Code: [Select]
type
  TCustomChartSeriesAccess = class(TCustomChartSeries)
  end;

function TChartListbox.CreateLegendItems: TChartLegendItems;
{ creates the a TLegendItems list and populates it with information for
  all series contained in the Chart. }
var
  skip: Boolean;
  s: TCustomChartSeries;
begin
  Result := TChartLegendItems.Create;
  try
    if FChart = nil then exit;
    for s in CustomSeries(Chart) do begin
      if Assigned(OnAddSeries) then begin
        skip := false;
        FOnAddSeries(Self, s, Result, skip);
        if skip then continue;
      end;
      TCustomChartSeriesAccess(s).GetLegendItemsBasic(Result);
      // Type-cast because GetLegendItemsBasic is protected.
//      s.GetSingleLegendItem(Result);
    end;
  except
    FreeAndNil(Result);
    raise;
  end;
end;

The crash is gone now, and the stacked bar series has multiple checkboxes. But clicking on one of these checkboxes hides the entire series, not just the particular bar belonging to this checkbox.

I have a solution in mind, but it would require styles to have a property "Visible". Could you implement this? Or is there another way to hide a "sub-"bar?

Another wish: Populating the chartsource of a stacked bar series would be easier if TListChartSource would have the following overloaded method

Code: [Select]
  function TListChartSource.Add(x:double; const y:array ouf double; ...): Integer;

just like TChartSeries. The current call of

Code: [Select]
i := ListChartChartSource.Add(x, y1);
ListChartSource.SetYList(i, [y2,y3,...]);

is a bit confusing.

Ask

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 687
Re: Stacked bar series
« Reply #6 on: June 08, 2012, 12:13:46 pm »
Quote
TChartListbox ... it crashes when assigned to a chart with stacked bars
This is actually a simple bug in the original implementation.
It crashed even with existing lmPoint multiplicity. Fixed in r37576.

Quote
Populating the chartsource of a stacked bar series would be easier if TListChartSource would have the following overloaded method

You can use AddXYList since r37580

Quote
I have a solution in mind, but it would require styles to have a property "Visible". Could you implement this? Or is there another way to hide a "sub-"bar?

Short answer is: yes, you can use TCalculatedChartSource.ReorderYList --
see multi demo since r37590.

The longer answer is quite involved ;-)
First I'd like to explain the reasoning behind TChartStyles design.
Current styles implementation is actually just a stab.
The idea was to create a mapping between series items and visual styles.

Both item selection and style description should be much more general:
for example, to highlight items based on value, apply gradients and transparency etc.

Styles can also be applied to different objects: for example,
currently axis stripes are implemented based on styles.
Another simple, although not yet implemented, application is
to assign different brushes to different slices of the pie charts.

The main roadblock for the implementation is that I was unable
to invent a suitable architecture both from API and implementation points
of view.
Thoughts and suggestions are welcome -- bonus points
for solving problems of applying visual effects on different back-ends
and backwards compatibility with the current code.

Based on the above, my thoughts are:
1) Styles do not necessarily correspond 1 to 1 with stack levels.
  For example, you can use just two styles to get alternating colors
  in a stacked bar of arbitrary height.
2) Nevertheless, using styles as a basis for legend makes sense,
  because it quite closely corresponds to the purpose of the legend --
  to identify various visual elements used in the chart.
2) Style.Visible, if implemented, should mean that the styled items are
  "transparent", as if having NaN value -- which is probably not what you want.
3) Style.Enabled, if implemented, should mean that the style is skipped during
  the search of applicable styles -- thus could be useful, but can be now
  achieved by setting RepeatCount = 0
4) As you can see from the demo, in specific case it it rather easy to implement
  hiding of a separate levels. In general case, it needs some thought.
  For example, one sensible UI might be a treeview with first level nodes for
  series and second level nodes for Y levels.

wp

  • Hero Member
  • *****
  • Posts: 13433
Re: Stacked bar series
« Reply #7 on: June 08, 2012, 05:23:55 pm »
Quote
This is actually a simple bug in the original implementation.
Strange. I have been using TChartListbox in several projects with single-leveled line series, and did not see a problem.

Quote
You can use AddXYList since r37580
Thank you. It is working fine.

Quote
... use TCalculatedChartSource.ReorderYList
I think that's fine for the special case in a project, but not a good solution for an independent component which cannot rely on the presence of TCalculatedChartSource on a form to work properly. This is getting really very complex.

Quote
3) Style.Enabled, if implemented, should mean that the style is skipped during the search of applicable styles -- thus could be useful, but can be now achieved by setting RepeatCount = 0
This sounds interesting. But regarding use of RepeatCount, I think an independent component like TChartListbox should not be allowed to alter a user-settable property thought for something else.

Quote
reasoning behind TChartStyles design
This seems to be a very interesting and flexible concept. But I get the impression that I am abusing it for the "simple" task to turn sub-bars on and off.

Another idea: How about a boolean array stored in the chart sorce which allows to turn y list elements on or off?
« Last Edit: June 08, 2012, 06:02:28 pm by wp »

Ask

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 687
Re: Stacked bar series
« Reply #8 on: June 09, 2012, 02:26:10 am »
Quote
I have been using TChartListbox in several projects with single-leveled line series, and did not see a problem.
Bug only manifested if you set Legend.Multiplicity=lmPoint

Quote
not a good solution for an independent component which cannot rely on the presence of TCalculatedChartSource on a form to work properly. This is getting really very complex.
Actually, the component might use the presense of calculated source as an indicator
that the user wants to control level visibility.
Otherwise all series with YCount > 0 will be displayed as a list of levels, which is bad,
especially for series like bubble and OHLC.

Quote
How about a boolean array stored in the chart sorce which allows to turn y list elements on or off?
That is theoretically possible, but will require a serious rewrite of all series and sources.
For example, YCount property will have to be split into RealYCount and VisibleYCount, list source
will have to establish some method of bi-directional translation since it allows user to modify its items,
some care will have to be taken in all sources to avoid extra copy in the default case, etc.

Also note that similar argument can be applied to other calculated source features -- for example,
the person who requested derivatives calculation might argue that implementing it in a separate
component introduces additional complexity *for him*, and so it should be included in all sources instead.
The whole idea of calculated source was to split rarely used features -- and I consider filtering levels
in stacked series relatively rare.

wp

  • Hero Member
  • *****
  • Posts: 13433
Re: Stacked bar series
« Reply #9 on: June 10, 2012, 12:25:40 am »
I see, all these ideas are not so good. Here's another one: It is my impression that these difficulties originate in the fact that the stacked bar (or area) series is one single  series, but uses different elements of the ylist. That's also the reason why creation of a chart with stacked series is quite complicated -- see first posting. So: the idea is to use individual series for each element of the stack. All series belonging to the same stack should share a common property "GroupIndex" to be added to TChartSeries. For normal bar charts, GroupIndex is 0, for stacked charts it is <> 0. The yList is not used in this concept. There must be a method which allows each series to determine its "base" -- that's the preceding series in the chart's SeriesList having the same GroupIndex (or nil, if GroupIndex=0). When drawing a bar, it should not begin at zero but at the top this "base" bar. Rearranging the sub-bars would be identical to rearranging the series in the object inspector tree. Calculation of the extent has to be modified such that the total y maximum is the sum of the ymax of all series sharing the same GroupIndex <>0. Maybe I am overlooking some more details, but the complexity does not seem to be too large. What do you think?


Ask

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 687
Re: Stacked bar series
« Reply #10 on: June 10, 2012, 06:58:36 am »
This design is very similar to the one in TeeChart,
which I specifically tried to fix.

First, consider the following use-case
(which is the only one I have encountered in my own TAChart applications):
There is a database table with N fields representing amounts
of money received by N different payment methods over some time period.
I want to plot these amounts as a stacked area series.
Current design requires one TDbChartSource, one TAreaSeries and
perhaps one TCalculatedChartSource in case I need to control visibility
of individual fields (I do not in this case).
You proposal will require me to create one TDbChartSource, N TCalculatedChartSource's and N TAreaSeries, which I consider a
serious complication.

Another use case, which I did not encounter with TAChart, but did
see in my previous work, is OLAP reporting. Consider a generic framework
for building reports based on more or less arbitrary queries to the multi-dimensional data set. Results of such reports are presented in either
tabular or chart form, in the latter case stacking is often used.
With the current TAChart implementation it is enough to create a single
series and a minimum of two styles to adequately represent results
of any query. If a separate series is required for each level, then
the application must dynamically create and destroy series depending on the query. It could be done, of course, but is certainly more work.

From the design point of view, we need to answer the following questions:
* what if data points from series' sources have different number of points and/or X values?
* what if properties of series are different? for example, ZPosition or BarOffset?
* what if series have different types (bars grouped with areas)?
* what should a user of chartlistbox do if he wants to list/control the entire series, and not individual levels (as I, for example, do)?

From the implementation point of view, it is a total rewrite of bar, area and, in part, line series. Currently each series is drawn independently from all others,
as opposed to axis, which are interdependent. You can look at any series drawing code vs. TAChartAxis/TAChartAxisUtils units to appreciate a
complexity which is added by that interdependency.

I the light of the above, while I am not against making usage easier for you,
I do not want to simultaneously make it harder for myself/others.

I can propose a compromise design which I will not implement,
but will consider for acceptance if implemented by you:
1) Create a separate TStackedBar, TStackedArea, TStackedLine series.
2) Create TChildSeries -- with TStackedBarChild, TStackedAreaChild, TStackedLineChild if needed.
3) Move properties which can be changed independently to child series -- for example, Color in TBarChild but BarWidth in TStackedBar.
4) Leave a common Source for parent series, but add TChildSeries.YIndex property
5) ChildSeries should return an empty extent, and have empty Draw procedure. All extent calculation and drawing should be performed by parents.
6) Note that while currently only these series have stacking, I plan to add more -- pie and polar series are prime candidates.
7) Finally, the above will require only a minimum modification to the chartlistbox -- add "ShowChildSeries" property, defaulting to false.

Yet another design, which I can probably implement:
1) Add TStackedChartLevel class with "Visible", "LegendText", "YIndex" properties
2) Add "StackLevels" collection to bar, area and line series
3) This will require somewhat larger chartlistbox change -- add "ShowLevels" property with specific code to show levels as opposed to series.

In both cases, child series or levels, if shown, should be somehow
visually grouped under parent series.

wp

  • Hero Member
  • *****
  • Posts: 13433
Re: Stacked bar series
« Reply #11 on: June 10, 2012, 11:17:17 am »
Quote
what if data points from series' sources have different number of points and/or X values?
what if properties of series are different? for example, ZPosition or BarOffset?
raise an exception

Quote
what if series have different types (bars grouped with areas)?
raise an exception if incompatible types have the same GroupIndex. Of course you can mix stacked bar and area series in the same chart if they have different GroupIndex values.

Quote
what should a user of chartlistbox do if he wants to list/control the entire series, and not individual levels (as I, for example, do)?
In a chartlistbox, all entries have the same level of priority - so, you'll be out of luck :-( A TreeView would be a better solution for this - I did this in a recent application (not at component level, though).

Quote
I the light of the above, while I am not against making usage easier for you,
I do not want to simultaneously make it harder for myself/others.
Just to make sure: there is no change in usage and behavior of the stacked series as long as GroupIndex is 0.

Quote
which I will not implement
Of course. I did not expect that

Quote
Yet another design, which I can probably implement:
1) Add TStackedChartLevel class with "Visible", "LegendText", "YIndex" properties
2) Add "StackLevels" collection to bar, area and line series
3) This will require somewhat larger chartlistbox change -- add "ShowLevels" property with specific code to show levels as opposed to series.
That sounds interesting. It is kind of similar to the Styles approach but avoids all the difficulties due to the wider usage of Styles. Shouldn't StackedChartLevel have a Style property also (making LegendText obsolete)?


 

TinyPortal © 2005-2018