Recent

Author Topic: TAChart: Changing standard time/date format for time axis  (Read 21706 times)

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
TAChart: Changing standard time/date format for time axis
« on: August 02, 2016, 03:41:38 pm »
In TAChart it is possible to show TTimeDate-Values on the X-Axis when using TDateTimeIntervalChartSource. The formatting of the labels is adjusted automatically according to the range, which works quite nice. The only drawback is, that it is not possible to distinguish the hh:nn and nn:ss, as both are seperated by a ':'. I would like to have a '.' here instead.

Examples, what a label actually shows:
mm.yyyy -> 08.2016
dd.mm -> 02.08
dd hh:nn -> 02 15:22
hh:nn -> 15:22
nn:ss -> 22:50
ssµµµms -> 50300ms
µµµms -> 300ms

What I would like to have (most critical for me is the nn.ss one):
mm.yyyy -> 08.2016
dd.mm. -> 02.08.
dd. hh:nn -> 02. 15:22
hh:nn -> 15:22
nn.ss -> 22.50
ss.µµµ
-> 50.300
.µµµ -> .300

Is this somehow possible to define?
If I change the write in the initialisation
DefaultFormatSettings.LongTimeFormat:= 'hh:nn.ss'; then nothing happens. I'm aware of that the formatting is related to the system settings, but it just doesn't look good in the way it is now. I guess the character defined in DefaultFormatSettings.TimeSeparator is used and there is no distinguishing between hh/nn and nn/ss.

Setting DateTimeFormat of TDateTimeIntervalChartSource is not a solution, as the dynamic formatting is disabled by that.

Thanks for your help!

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #1 on: August 02, 2016, 09:16:41 pm »
Just uploaded a new version of TDateTimeIntervalChartSource which supports individual formatting of the date/time steps selected depending on axis range. The new property is called DateTimeStepFormat and has subproperties for year, month, etc steps. It is maybe a bit difficult to find the correct property if you want to modify a particular format string. Have a look at the axis demo, page "Date and time"; here you can activate some alternate format strings; a label displays the date/time step identifier currently selected. Maybe it gets clearer then which format you have to adapt.

The format strings follow the syntax needed for the FormatDateTime function (http://www.freepascal.org/docs-html/rtl/sysutils/formatchars.html). Note that milliseconds are not specified by the symbol "µ", but by "z". The symbols "/" and ":" are replaced by the date and time separators of the DefaultFormatSettings, respectively. Beyond that, DefaultFormatSettings are ignored here. You can enclose any other text by quoting it. So, if you want a slash in a date, but your DateSeparator is "." then the format string must be dd"/"mm"/"yyyy - without the quotes the / would be replaced by a .

Another Example (not used in the demo): s","zzz" sec" writes the seconds, a comma as decimal separator, then three digits for milliseconds, and the unit string "sec".

You must use Lazarus trunk to get this new feature. Because it is a new feature it will not be backported to the upcoming Lazarus 1.6.2. So, if you don't want to use trunk you'll have to wait for Laz 1.8 or 2.0...

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #2 on: August 03, 2016, 09:15:11 pm »
Thanks alot for the detailed reply!
I'll have a look.

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #3 on: August 03, 2016, 10:34:03 pm »
I did a first test and it works really nice! From your long description I thought it is more complicated, but the properties are quite intuitive! The only thing to be careful is with the quotation marks ". I just used them for all formattings to be sure it doesn't change behaviour on other systems.

Here are my settings, leading to the desired formatting I mentioned in the first post:
Code: Pascal  [Select][+][-]
  1.   DateTimeIntervalChartSource.DateTimeStepFormat.MillisecondFormat:= 's"."zzz';
  2.   DateTimeIntervalChartSource.DateTimeStepFormat.SecondFormat:= 'nn"."ss';
  3.   DateTimeIntervalChartSource.DateTimeStepFormat.MinuteFormat:= 'hh":"nn';
  4.   DateTimeIntervalChartSource.DateTimeStepFormat.HourFormat:= 'dd"." hh":"00';
  5.   DateTimeIntervalChartSource.DateTimeStepFormat.DayFormat:= 'dd"."mm"."';
  6.   DateTimeIntervalChartSource.DateTimeStepFormat.WeekFormat:= 'dd"."mm"."';
  7.   DateTimeIntervalChartSource.DateTimeStepFormat.MonthFormat:= 'mm"."yyyy"';
  8.   DateTimeIntervalChartSource.DateTimeStepFormat.YearFormat:= 'yyyy';

There seems one mistake in the default settings, the default for SecondFormat is hh:ss instead of nn:ss resulting in the (wrong) outline of hours:seconds instead of minutes:seconds.

One further nice to have in the formatting would be an optional clearance for Axis.Marks.OverlapPolicy:= opHideNeighbour (in the Axis settings, nothing to do with the TDateTimeIntervalChartSource). Now two labels can get randomly close as long as there is no overlapping. Or is there already a property to realise that?

Thanks again, the chart component is really great!

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #4 on: August 03, 2016, 11:05:42 pm »
Fixed the typo, sorry.

Overlapping labels is a new story. You'll find the corresponding properties in the Params of the DateTimeIntervalChartSource (OverlapPolicy gives you less control). There are several conflicting options, and it is a bit difficult to describe. The "MinLength" is the smallest pixel distance between label positions. If two successive labels are closer then the algorithm selects the next wider interval. Similarly with "MaxLength". So, in order to get less crowded labels you should increase the MinLength. I cannot remember about the priorities, maybe you seek here in the forum the post where I asked a similar question regarding the axis Intervals to Alexander Klenin ("Ask") who wrote this feature.

What I am missing is an option to set labels at "nice" positions, i.e. I would like to force month labels to be at the first day of a month, not somewhere in between. But I don't have a good idea so far.

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #5 on: August 04, 2016, 10:30:08 pm »
Hello WP,

the "MinLength" and "MaxLength" - as far as I know - are adjusting the intervals of the grid. But I only want to have less labels in case of overlapping or when they are too near, the grid should stay the same.

Below some offtopics, but as the initial question is already solved, I guess thats allright~~

I think for numeric values (i.e. of the decimal system) the distance between each grid bar should always be the same for visual purpose. I.e. it just looks better. For time values this could work the same, but date values with the 31, 30 and 28 days per month cannot suit that, different distances between the grid is a must otherwise one label would be March 1st, the next March 7th 18:00, the next March 15th, 12:00 etc. I guess thats also the issue for the NiceSteps-algorithm, that it rather arranges the grid in same distances than picking out the first of each month.

On the other hand for numeric values in my opinion the easiest way to define a nice looking axis is to give the left bound, the right bound and the spacing (here: Intervals.Count). With these three values the user can fully control each label position (make his own "NiceSteps") and NiceSteps would not be needed anymore. The distances in this case then should be actually all the same. For panning, the labels and grid should float simultaneously, so the label value would always be the same. When zooming, the labels and grid should also stay at the same value (not position). When zooming-in therefore the number of labels will be decreased to a certain point. When they get too rare they could be doubled, means the grid intervals get half distance. When zooming out the same vise versa. So for example a spacing of 5 is defined, when zooming in there will be first just 4 grid bars but when reaching a zoom level with just 3 bars, then the grid distance is cut in half resulting in 6 grid bars. In this way the labels would always be on a 2^n-fraction of the "home"-labels. It means, the grid policies in zoomed mode would be different to a non-zoomed one. For decimal data this would be very nice, I think. Even for a logarithmic scale this could be adaptable, I guess. If the "distance" (actually difference) of the logarithmic value intervals is always the same, then it would be possible to distribute the grid in a predictabel (logarithmic) way.

Now this was just some imagination of mine, I couldn't contribute here, as the TAChart component is a black box to me. So if you are not interested in my thoughts to that topics then just don't mind :-)

Regards

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #6 on: August 05, 2016, 12:54:40 am »
I know that the current axis labelling method is not perfect. For date/time, the issue is that the DateTimeIntervalChartSource is an "Interval"ChartSource, i.e. intrinsically tries to get constant intervals. This makes it impossible to get nice dates at the month and year levels. The other source of headache is the log scale which is unpractical for a scientific or technical user. As a workaround I proposed in many related posts here to pack all labels into a TListChartSource which you must create and populate on your own - a little piece of work, but you have full control over where labels are put. A general solution for zooming and panning, however, will soon get very complex (you can find some posts here).

Your idea of freezing the axis limits and using a given count of labels is already possible today. Use Axis.Range to define the limits of the axis, specify the number of labels in Axis.Intervals.Count and disable the other labelling criteria, i.e. set Axis.Intervals.Options = [aipUseCount]. In case of the date/time chartsource you must use its Params instead of the Axis.Intervals.

This does not work with zooming because unlike what you propose zooming can make the axis start and end at any value which makes the power of 2 approach unfunctional.

If you want to avoid overlapping labels but keep the day-to-day grid, for example, you can consider adding a second x axis with labels turned off and grid turned on and with a ListChartSource containing only day values.

But anyway - all solutions which you design for your current project are standalone solutions and nothing but workarounds for a deficiency of TAChart. Maybe I should take the time to re-think a better way.

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #7 on: August 05, 2016, 10:35:12 am »
As a workaround I proposed in many related posts here to pack all labels into a TListChartSource which you must create and populate on your own - a little piece of work, but you have full control over where labels are put.
For a non-zoomable Chart this is perfectly fine. Just when zooming is performed, then the overall impression suffers. but as you said, the implementation gets very complex...
Besides, the existing functionality actually is very powerful.

Your idea of freezing the axis limits and using a given count of labels is already possible today.
I actually use these settings already. For the standard view its great, just when panning and zooming, the label values are getting really odd (of course...).

This does not work with zooming because unlike what you propose zooming can make the axis start and end at any value which makes the power of 2 approach unfunctional.
My idea is always to use the power of 2 values, no matter at which value the axis starts and ends, it means the labels don't stick to the axis borders. In this way when zooming in, one could actually see zooming into the grid. Now the grid sometimes is jumping around.
Example: Bounds 0 to 10 with spacing 5 results in the grid/labels:
0; 2; 4; 6; 8; 10
Zooming-in to bounds 3,36 to 5,14 would result in grid/labels:
3.5; 3.75; 4; 4.25; 4.5; 4.75; 5

I guess I'll try to understand how the ChartSources work, perhaps (just perhaps) I can make an example implementation then.

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #8 on: August 05, 2016, 12:58:11 pm »
Example: Bounds 0 to 10 with spacing 5 results in the grid/labels:
0; 2; 4; 6; 8; 10
Zooming-in to bounds 3,36 to 5,14 would result in grid/labels:
3.5; 3.75; 4; 4.25; 4.5; 4.75; 5
What is the algorithm how to calculate the new spacing here?

In my understanding there is a problem with the power-of-2 approach: It leads to "ugly" labels, such as the 0.25 interval from above example, probably also 0.125 and 0,0625 etc if zooming more. The aipNiceSteps Option tries to stick to 1/2/5 labels.

Yes, an example of your idea would be very welcome. Just add a TListChartSource to the form, link it to the Marks.Source of an axis and set the Marks.Style to smsLabel. Write a method "PopulateLabels" (or similar) in which you
  • clear the ChartSource (ListChartSource1.Clear)
  • query the extent of the axis (var ext: TDoubleRect; ext := Chart1.LogicalExtent)
  • and add the axis values which should have a label (apply it to both x and y to make the chartsource usable also in case of rotated axes): ListChartSource.Add(newpos, newpos, text_at_label). Calculate the newpos values according to your algorithm knowing the extent limits (ext.a.x and ext.b.x for the bottom axis)
Call this whenever the chart's extent changes (OnExtentChanged event).

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #9 on: August 07, 2016, 12:59:46 pm »
In my understanding there is a problem with the power-of-2 approach: It leads to "ugly" labels, such as the 0.25 interval from above example, probably also 0.125 and 0,0625 etc if zooming more. The aipNiceSteps Option tries to stick to 1/2/5 labels.
Thats right, when zooming-in a lot then there will also be ugly labels. But the advantage is, that the grid really always is constant.
The problem with aipNiceSteps is, that this is an automatic axis arrangement, i.e. you loose control over the positions. With my method if you want to place labels on the positions 0; Pi/2; Pi; 1.5Pi;2Pi then you simply can set XMin:= 0, XMax:= 2Pi and Intervals.Count:= 4. The labels will be ugly in this case, but the grid is as desired (fully under owns control). I know the aipNiceSteps can be adjusted to ones needs and for the software developer thats fine, but if you provide a chart in your application where the user can display random data, then the settings should be as simple as possible.

Nevertheless I see the problem of the 2^n approach and tried also a decimal one with (10^n [+5] [+2]). 5 and 2 are optional and used according to the zoom factor, to keep the number of grid/labels in a better range.

In the attached demo you can select either of them for the X-axis. For comparison purpose the Y-axis is configured with aipUseNiceSteps.

In my approach the grid always depends on the unzoomed settings, thus if the bounds and spacing are choosen bad (e.g. xmin=0, xmax=10, spacing=3) then all sub grids will be bad.

What is the algorithm how to calculate the new spacing here?
Difficult to describe and probably not the optimal implementation, but it works~
Basis is always the unzoomed grid. First I calculate the (constant) grid distance, to do that the ratio between the unzoomed extent and the zoomed one is used. In the zoomed-in state (Ratio < 1) the ratio is multiplied by the factor 2 until the Ratio gets a value of > 0.7 . In the same number of multiplications the unzoomed grid distance is divided by 2 (halving the distance). The value 0.7 comes from the number of grid bars/labels: The quantity shouldn't get less than 0.7 times the number in the unzoomed state. Zooming-out works in the same way, but vise versa.
With this grid distance I determine the first visible grid bar within the zoom range and then just allign one by one in the same distance.

The decimal approach works quite similar.

In my implementation I derive from a TIntervalChartSource. The problem is that I don't know how to get the bounds of the unzoomed state. Especially when using transformations this could become troubblesome. Any idea here? In the demo I took the Chart.Extent.Xmin and Xmax directly, so this is really just a demo. One possibility would be to define the values in the ChartSource-component itself. Disadvantage is, this would need double definition of the bounds, once in Chart, once in the ChartSource-component. Advantage is, automatic axis ranges would be possible.

Regards~

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #10 on: August 07, 2016, 08:54:05 pm »
Here an update of the demo. The class TFixedIntervalChartSource now is derived from TCustomChartSource, there are three properties to define the nonzoomed extent now: GridCount, DefaultExtentMin and DefaultExtentMax. The actual extent of the chart still has to be defined within the extent properties. Another property IntervalMode can have the value imDual for the 2^n approach and imDecimal for improved outline. With these properties there is no more internal dependency to the demo-chart, so it could be easily used for projects.
It seems also to work with linear transformed axis (others not tested).

Usage:
Code: Pascal  [Select][+][-]
  1.   XIntervalSource:= TFixedIntervalChartSource.Create(Self);
  2.   XIntervalSource.IntervalMode:= imDecimal;
  3.   XIntervalSource.GridCount:= 5;
  4.   XIntervalSource.DefaultExtentMin:= 0;
  5.   XIntervalSource.DefaultExtentMax:= 10;
  6.   Chart1.BottomAxis.Marks.Source:= XIntervalSource;

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #11 on: August 08, 2016, 12:17:32 am »
Very interesting, really!

Initially, I was confused by the naming (TFixedIntervalChartSource), and by your description of a constant count of labels, because after zooming the count of labels changes. But after some playing I was beginning to see the point: If you zoom around a labeled point, then the label is still existing afterwards. In the builtin algorithm, however, this label usually disappears after zooming. This is a very interesting feature. To avoid the confusion with the name I'd propose a more general name, such as TAxisChartSource.

Of course, it would be great if your algorithm would work also with nonlinear transformations as well. An interesting case would be a log scale. Suppose an axis between 1 and 1000; on log scale the axis limits are 0 and 3. I'd expect to see primarily labels at the integer parts of the graph units (0, 1, 2, 3), i.e. powers of 10 of the axis units (1, 10, 100, 1000). Now, if zooming happens, additional values may be inserted in between, but in order to get "nice" labels these additional labels will be at nonequidistant points (e.g: 1, 3, 10, 30 which in graph units corresponds to 0, 0.477, 1, 1.477)

For working with transformations, the TValueInRangeParams passed to the ValuesInRange method contains fields with function pointers for the current transformations and their inverse: FAxisToGraph converts from axis to graph units, and conversely FGraphToAxis converts from graph to axis units. And there is also a FGraphToImage function pointer which converts the graph units to drawing units (usually pixels) (and a final conversion FScale from these units to final device units, as needed e.g. for printer output with a much higher pixel density).

The ValuesInRangeParams contain also a field FFormat, you should use this to calculate the label (instead of FloatToStr).

Using dedicated DefaultExtentMin/Max values for the ChartSource seem to pose some limitation to me because it does not take care of the automatic axis range determination. I see that there is not direct way for the ChartSource to get the unzoomed axis limits. Therefore I modified TAChart such that two more fields (e.g., FFullExtentMin and FFullExtentMax) are available in the ValuesInRangeParams. It is not yet in the official sources because I don't know where this discussion will lead us. Please copy the attached files into your TAChart folder (make a backup copy first). Note that the modified files are from the trunk version of Lazarus, I don't know if 1.6 will compile with this modification.

In the second attachment you find a modified version of your demo where the DefaultExtentMin/Max properties are removed.

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #12 on: August 08, 2016, 10:32:20 am »
The ValuesInRangeParams contain also a field FFormat, you should use this to calculate the label (instead of FloatToStr).
The required code is already written in your local AddValue function, just uncomment it. This would have the advantage of respecting the format specified by the axis' Marks.Format.

You should also override the inherited method GetCount to return 0. This prevents a crash if the user calls one of the inherited Extent* methods (due to calling an abstract method).

kupferstecher

  • Hero Member
  • *****
  • Posts: 583
Re: TAChart: Changing standard time/date format for time axis
« Reply #13 on: August 08, 2016, 10:33:09 pm »
For the naming thing we can change it, no problem, its always difficult to find a good name~

Using dedicated DefaultExtentMin/Max values for the ChartSource seem to pose some limitation to me because it does not take care of the automatic axis range determination. [...] Therefore I modified TAChart such that two more fields (e.g., FFullExtentMin and FFullExtentMax)
How are the values FFullExtentMin/Max determined? The problem is, if the autoscaling is used, then the label values are not predictable and will result in ugly values. E.g. the automatic range is from 0 to 3 and GridCount is set to 5 then the labels will be 0; 0.6; 1.2; 1.8 etc. and all sub labels would be similar bad. You can see that behavior when adding the class to the Y-axis in your modified demo.
For me an automatic ranging should be treated in the same way as zooming. So in my opinion the dedicated parameters are even for autoscaling actually useful. Because the label values will always stay the same as defined, just the quantity and intervals may change.

Of course, it would be great if your algorithm would work also with nonlinear transformations as well. An interesting case would be a log scale. Suppose an axis between 1 and 1000; on log scale the axis limits are 0 and 3. I'd expect to see primarily labels at the integer parts of the graph units (0, 1, 2, 3), i.e. powers of 10 of the axis units (1, 10, 100, 1000).
I tested for the logarithmic scale without changing anything in the unit, it actually works, the grid results in decreased distance. But of course its not in the way you pointed out. I think therefore a seperate ChartSource-class is needed. Normally the grid for a logarithmic scale is something like 1;2;3;...;9;10;20;30;...;90;100;200; etc. But labels might only be at 1;10;100;1000. A problem is how to handle zooming. If grid distance should be increased then how could it be? 1;3;5;7;9;10;30;50...? Not so nice, between 9 and 10 it is a different quality then the others. 1;4;7;10;40;70...? This seems quite ok, but grid bars are much less.

When zooming in (or out) things get interesting. Perhaps its possible to do it in a similar way like in the TDateTimeIntervalChartSource, where always the next smaller unit is resolved. It means for bounds between 9 and 12 the grid could be 9;9.1;9.2;9.3...9.9;10;11;12. And number of grid/labels again trying to determine according a (not strict) parameter GridCount. But if the range is from 7 to 12 then we suddenly need the subgrid for 7, for 8 and for 9. Seems really not so easy...

The ValuesInRangeParams contain also a field FFormat, you should use this to calculate the label (instead of FloatToStr).
I just commented out, because it gave me a compile error. I leave it first until the parameters and algorithms are fixed. Same with GetCount. But thanks for mentioning! I probably wouldn't have recognised.

wp

  • Hero Member
  • *****
  • Posts: 11917
Re: TAChart: Changing standard time/date format for time axis
« Reply #14 on: August 09, 2016, 11:24:30 am »
How are the values FFullExtentMin/Max determined? The problem is, if the autoscaling is used, then the label values are not predictable and will result in ugly values.
Then this a severe weakness of the algorithm, it MUST be able to produce better results than the builtin algorithm automatically.

BTW: How does your algorithm behave if max is positive and min is negative? Does it produce a label to be at 0?

Here is my idea. It is based on the observation that your algorithm produces "nice" starting points. My algorithm calculates a "nice" starting point and interval:
  • Calculate range R = max - min (e.g. max = 123, min = 101 --> R = 22)
  • Calculate estimated Interval I' = R / GridCount ( --> if GrindCount = 5 --> I' = 22/5 = 5.5) -- note: GridCount may change according to the situation.
  • Calculate mantissa M and exponent E of estimated interval (M = 5.5, E = 0)
  • If M > 5 --> Label interval I = 5 x 10^E
  • IF M > 2 --> Label interval I = 2 x10^E
  • If M > 1 --> Label interval I = 1 x 10^E  ( --> I = 5)
  • Round axis min to be a multiple of I (round down): Min = floor(min / I) * I  (--> Min = 100)
  • Add intervals until value is > max ( --> 100, 105, 110, 115, 120, 125)
Another example: min = 100.1229, max = 100.1256  --> R = 0.0027 -->  I' = 0.00054 --> M = 5.4, E = -4 --> I = 0.0005 --> labels at 100.1225, 100.1230, 100.1235, 100.1240, 100.1245, 100.1250, 100.1255, 100.1260

And a final example enclosing the zero: min = -98, max = 23 --> R = 121 --> I' = 24.2 --> M = 2.42, E = 1 --> I = 20 --> labels at -100, -80, -60, -40, -20, 0, 20, 40

Note that the first and last label are always beyond the real axis range. They will not be painted anyway, but are required in order to fill the space to the first/last visible label with minor labels if needed.

As for logarithmic scaling I tried to implement the following idea several times but never completed it:
  • There are two calculation modes, based on direct and transformed (log) values
  • Define a transition point, i.e. say: range is 2 decade
  • If range is > 2 decades ("transition point") work on log values. Integer parts of logs define the axis labels --> Label = 10^integer. If range is very large then it may be required to skip one or more labels (check for overlapping labels?)
  • If range is < 2 decades use your/my algorithm with direct values
« Last Edit: August 09, 2016, 11:33:55 am by wp »

 

TinyPortal © 2005-2018