Skip to content
g$ edited this page Jun 19, 2018 · 27 revisions

As should be no surprise, value labels are a separate component you configure into the yacc:Components collection, at the desired stacking position. Also notice that there's only one label component; it can label anything!

Value Source

Labels have a source (via the SourceName property) which is a ChartComponent that provides item values (see below).

Channels

A source may be tracking multiple values, like a StackedColumnSeries, CandlestickSeries or HorizontalBand. Instead of making up specialized names for each value (indeed the StackedColumnSeries may have an arbitrary number of values), they are simply numbered, and each one is called a Channel (think: Data Acquisition). The default channel is zero; additional channels increment from there.

The label component can attach to any channel supported by its source, using the ValueChannel property.

Interfaces

The core of the label infrastructure starts with the source-implemented IProvideSeriesItemValues and ISeriesItem interfaces. This provides a "uniform" view of a component's data. Any ChartComponent may implement this, so any component that tracks a value can be labelled.

Different components extend their ISeriesItem via additional interfaces that describe "slices" of features:

  • ISeriesItemValue tracks a single value/channel, e.g. ColumnSeries, HorizontalRule.
  • ISeriesItemValues tracks multiple values, e.g. StackedColumnSeries, HorizontalBand.
  • IProvidePlacement has geometry information for enhanced placement.

Label Positioning

Positioning a label is a two-step process, which occurs in two different coordinate systems! This may seem confusing, but makes sense when you see the results.

  • Placement: offset occurs in world coordinates, because it "naturally" matches the data you are labeling.
  • Label: offset occurs in device coordinates, because it "naturally" matches the XAML coordinates of the label's TextBlock.

Important: the y-axis of these two systems go in opposite directions!

Positioning also uses a concept called the half dimension, which is simply the width and height divided in half. The units used are the same as their respective step. This is also "natural" because when centered, exactly half the width and height are on either side of the center point!

This choice of basis makes an NDC for uniform positioning adjustments around a center point that is transform-invariant.

Placement Step

The first step is placement, which occurs in world coordinates, the same as your data values. This step locates a point on the series geometry for placing the label (in the second step below).

For unhinted series, there is no placement step, e.g. MarkerSeries; the placement coordinate is at the data value, and only the Label step is applied.

For hinted series, the placement is the "center point" of a geometry, typically a Rect. The coordinates extend in the unit direction along both axes, and is scaled by the rectangle's half-dimension, such that:

  • (0,0) is the center point.
  • (0,1) is the "ending" point (along y-axis).
  • (0,-1) is the "starting" point (along y-axis).

The reason for "starting" and "ending" is related to the next placement data, the direction of the geometry. For example, a column that represents a negative value starts at zero and ends at a negative number i.e. it points "down", otherwise it points "up".

Label Placement Phase 1

This accounting for the direction allows the placement offset to be the same, and still produce the desired results: mirrored y-axis placement. For example, a label "outside" a rectangle, remains so on either side of the y-axis zero line. A simple non-mirrored offset would place the "upper" labels outside, but the "lower" labels inside!

Label Step

The second step is label, which occurs in XAML device coordinates, the same as the label's TextBlock. The starting point for this step, is the world coordinate that results from the Position step (if used), otherwise is the data value coordinate.

The placement world coordinates are translated into XAML device coordinates, and this becomes the center point for the TextBlock. The LabelOffset is used to move the TextBlock to its final position.

The default values of PlacementOffset and LabelOffset create a label placement where the TextBlock is directly centered over the placement coordinates, which typically ends up in the "center" of the series geometry.

Label placement starts from the "center point" of the TextBlock, and extends in unit direction along both axes, and is scaled by (half-dimension of) the actual size of the TextBlock, such that:

  • (0,0) is the center point.
  • (0,1) shifts the label "up" (visually down but up in y-axis coordinates) by its half-height.
  • (0,-1) shifts the label "down" (visually up) by its half-height.

The label step also takes account of the geometry's direction, so the label moves in a matching direction of the position.

Label Placement Phase 2

Label Generation

Label generation follows a fixed pipeline, with these customization points:

  • selection
  • format
  • style

Rather than re-create the wheel here, YACC uses the ubiquitous IValueConverter interface to perform the heavy lifting.

The IValueConverter.Convert method is used, and the targetType parameter is used to "encode" which aspect of the pipeline is being queried.

  • bool determines whether to continue with the label generation for this value,
  • Tuple<Style,String> determines whether custom styling and/or formatting is desired.

The value parameter is used to pass a context interface (ILabelSelectorContext), by which the converter gains access to the item being processed. See the source code examples below.

It is possible to "slice and dice" these up among as many or as few IValueConverter implementations as desired.

Selection

Before a label is created, there is a chance to first decide whether this should occur. If the LabelSelector property is assigned, then it is consulted with bool as the targetType parameter.

  • true (or not null) indicates the label should be created,
  • false (or null) indicates the label should not be created.

Here's an example from the demo application. It "selects" the values that correspond to the series extents (min/max):

public class MinMaxObservationValueConverter : IValueConverter {
	public object Convert(object value, Type targetType, object parameter, string language) {
		if (value is ILabelSelectorContext ilssc) {
			if (targetType == typeof(bool)) {
				if (ilssc.Source is IProvideValueExtents ipve && ilssc.ItemValue is ISeriesItemValueDouble isivd) {
					// finally checked things enough to do something!
					return isivd.Value >= ipve.Maximum || isivd.Value <= ipve.Minimum;
				}
			}
		}
		return true;
	}
	public object ConvertBack(object value, Type targetType, object parameter, string language) {
		throw new NotImplementedException();
	}
}

Since it's interfaces "all the way down" in YACC, the sample makes liberal use of the extended is capture syntax to tweeze apart the necessary players to perform its "business logic".

Style and Format

There are several ways to style labels:

  • with no configuration, rely on the Theme styles,
  • for static styling, create your own Style and assign to the LabelStyle,
  • for dynamic styling, create an IValueConverter and assign to the LabelFormatter.

In addition, it's possible to dynamically create an alternate label. Applying the ValueLabelPath to return the data source objects, this opens up the possibility to format an alternate value for the label text.

Formatter

When specified, ValueLabels calls the formatter, via IConvertValue.Convert with different values of the targetType parameter, to indicate what aspect of the label is being queried:

Starting in 1.5.x the targetType parameter is Tuple<Style,String> with one call for both values.

Populate the returned Tuple as follows:

  • Item1: Style, return a custom style to replace the "default" or null to opt out.
  • Item2: String, return an alternate label text to replace the "default" or null to opt out.

The value parameter is always an instance of ILabelSelectorContext, which is used to make decisions about what to return. The parameter parameter is always null, and the language is taken from CultureInfo.CurrentUICulture.

Static Styling

When using the LabelStyle property, keep in mind that this is a DependencyProperty, and all the generated elements (internally) bind to this property, so changes should propagate in the usual way.

Dynamic Styling

The most possibilities are realized using dynamic styling via the LabelFormatter property.

Here is an example from the Demo Application:

public class CompareObservationValuesConverter : IValueConverter {
	public Style WhenGreater { get; set; }
	public Style WhenLess { get; set; }
	public Style WhenEqual { get; set; }
	public object Convert(object value, Type targetType, object parameter, string language) {
		if(value is ILabelSelectorContext ilssc) {
			if(ilssc.ItemValue is ISeriesItemValueCustom isivc && isivc.CustomValue is Observation obv) {
				if (targetType == typeof(Tuple<Style,String>)) {
					// select the style based on the difference
					// format the label based on the difference
					// select the "trend indicator" in Unicode arrows
					// Left Right Arrow (U+2194)
					// Down Arrow (U+2193)
					// Up Arrow (U+2191)
					if (obv.Value1 < obv.Value2) return new Tuple<Style,String>(WhenLess, Format(obv, "\u2193"));
					else if (obv.Value1 > obv.Value2) return new Tuple<Style, String>(WhenGreater, Format(obv, "\u2191"));
					return new Tuple<Style, String>(WhenEqual, Format(obv, "\u2194"));
				}
			}
		}
		return null;
	}

	public object ConvertBack(object value, Type targetType, object parameter, string language) {
		throw new NotImplementedException();
	}
}

Pay special attention to the "checks" which extract the values required to do the actual styling task. Using the special is syntax ensures no null values leak into the innermost section.

The rest is straightforward. Simply include your shiny new converter in the Resources:

<vm:CompareObservationValuesConverter x:Key="ColorLabel"
	WhenEqual="{StaticResource BigLabels2}"
	WhenGreater="{StaticResource v1_GreaterThan_v2}"
	WhenLess="{StaticResource v1_LessThan_v2}"/>

and reference from the ValueLabels you wish to control:

<yacc:ValueLabels SourceName="colv1" LabelFormatString="F2" CategoryAxisOffset=".375"
	PlacementOffset="0,1" LabelOffset="0,1" LabelStyle="{StaticResource BigLabels}"
	LabelFormatter="{StaticResource ColorLabel}" LabelSelector="{StaticResource MinMaxLabel}" />

Accessing Data Source Objects

You should note that the style selector is accessing the Observation instance that was originally seen by the ColumnSeries during processing. This is done by using the ValueLabelPath property, and setting it to "." (period). This is handled as a "signal" to perform a RelativeSource binding, which ends up returning the binding object itself, as opposed to a property of said object.

Clone this wiki locally