The DataSetViewer can be extended with new visualizations. Basic set of visualizations is presented in the
DynamicDataDisplayViews.dll, but the DataSetViewer uses the
Managed Extensibility Framework (MEF) to find custom visualizations represented as .NET class libraries located in the
Visualizations subfolder of the application folder. Therefore, to add new visualization you should (i) implement several specific classes, transforming data into a visual representation, and (ii) supply the assembly into the
Visualizations folder. At the application start up, the visualization will be loaded as well as basic visualizations.
Implementing visualization
From the architectural point of view, there are two main notions, required to develop a visualization.
- View is a place where visualizations happen. It provides an infrastructure enabling specific type of visualizations. For example, a view may consist of a table, or a set of controls representing coordinate axes, ticks, grid etc. Thus, a view restricts available type of visualizations; e.g., if a table view is shown by the DataSetViewer, it allows only table visualizations.
- Visualization is an object which transforms data into a specific visual representation. A visualization must be added into an appropriate view and then it uses the view's content to make a representation. So, many visualizations of an appropriated type may be added into a single view and shown together (e.g. several line graphs in a single coordinate system).
- VisualizationStyle is an object which exposes visualization properties, provides information how they can be properly set, and produces a visualization instance, when all properties are set.
- Visualization factory implements IPrimitiveVisualizationFactory interface and produces visualization style instances on demand. A factory is loaded by MEF and is an entry point for the new visualization library.
Views
View is an object of type derived from the abstract
Microsoft.Research.Science.Data.Viewer.VisualView class, located in DataSetViewerCore.dll. VisualView is a WPF ContentControl; its content depends on particular type of supported visualizations. Basically DataSetViewers has two main types of VisualView:
- TableView contains a table control, whose columns correspond to visualizations, respectively.
- DynamicDataDisplayView (D3 View) contains a ChartPlotter instance (a drawing control of the D3 library). Each visualization, when attached to the view, may add a single plotter element (i.e. a graph) into the view's plotter; thus there is a set of visualizations, named D3 visualizations, those can be added to the D3 View.
There are several types of D3 views:
- DynamicDataDisplayViewSingleAxis is a view for visualizations aimed at 2d data, such as line graphs, markers.
- DynamicDataDisplayViewTwoAxes is a view for visualizations aimed at 3d data, such as heatmaps, isolines.
- GeoView is a view, derived from the DynamicDataDisplayViewTwoAxes, which adds a background map layer.
All D3 Views provide coordinate axes with ticks, background grid, and navigation using mouse and keyboard.
Next, we will describe steps how to create new D3 visualization, i.e. a visualization for one of the D3 views.
Creating a D3 visualization
1.
Select a base class for the new visualization class. For D3 visualizations, this should be one of available in the
DynamicDataDisplayViews.dll (namespace
Microsoft.Research.Science.Data.Viewer.DDD):
Base class | Destination |
DynamicDataDisplayVisualizationSingleAxis | Base class for 2d D3-based visualizations of 2d data (i.e. table data), when Y-values depend on X-values. For example, line graph, markers, bar chart etc. |
DynamicDataDisplayVisualizationTwoAxes | Base class for 2d D3-based visualizations of 3d data, when Z-values are shown in (X,Y) region. For example, heatmap, isolines (contour lines) etc. |
DynamicDataDisplayPaletteVisualization | Base class for 2d D3-based visualizations of 3d data; it derives from DynamicDataDisplayVisualizationTwoAxes. The Palette style property is added and legend thumbnail is set. |
2.
Add new class, derived from the selected base visualization class, and add an attribute
StyleName with name of the visualization to be used in visual hints.
[StyleName("Polyline")]
public class LineGraphVisualization : DynamicDataDisplayVisualizationSingleAxis
3.
Add a constructor to the class with custom parameters, such as data parameters (as SDS Variables) and a mandatory
VisualizationStyle object which represents for which style the visualization is created and must be passed to the base constructor. Parameters of the constructor depend on a visualization; in the example below the visualization class needs for two variables with data considered as "values" and "grid"; also it might need for a dimension instead of a variable as an axis. Therefore, create one or several constructors with parameters you need, but note that a base class has non-default constructors and you should pass it required parameters in any case.
public LineGraphVisualization(Variable displayVar, Variable axisVar, VisualizationStyle style)
: base("Polyline", displayVar, axisVar, style)
{
if (displayVar == null) throw new ArgumentNullException("displayVar");
if (axisVar == null) throw new ArgumentNullException("axisVar");
if (!AreCorrect(displayVar, axisVar)) // visualization-specific method that checks whether input variable are correct for the visualization
throw new ArgumentException("Variables are not supported by the visualization");
horizontalAxis = ViewAxis.GetViewAxis(axisVar); // setting the axes (view will use them to check visualization compatibility issues, i.e. whether 2 visualizations can be shown on the same view, or not)
verticalAxis = ViewAxis.GetViewAxis(displayVar);
// horizontalAxis = new DimensionAxis(dimension); // this must be used when horizontal axis is based on a dimension
}
The mentioned in the example
AreCorrect() method may use inherited
IsMine(Variable) method which checks whether a display variable has rank 1 and supported type (numeric).
3.1.
Parameters of the DynamicDataDisplayVisualizationSingleAxis constructor.
Parameter | Description |
string typeName | A name that is used to produce descriptive string about the visualization. |
Variable dispVar | A 1d-variable that contains Y-values. Cannot be null. |
Variable axisVar | A 1d-variable that contains X-values. If not null, must depend on the same dimension as dispVar; otherwise, if null, X-values are integer indices of the dimension of the dispVar, e.g. 0,1,...,length(dimension)-1. |
VisualizationStyle style | Styles are described below. Here we say that this parameter must be presented in the constructor of your visualization and passed to the base class as is. |
3.2.
Parameters of the DynamicDataDisplayVisualizationTwoAxes and DynamicDataDisplayPaletteVisualization constructors.
Parameter | Description |
string typeName | A name that is used to produce descriptive string about the visualization. |
Variable dataVar | A 2d-variable that contains Z-values. Cannot be null. |
Variable varX | A 1d or 2d variable that contains X-values. If not null, must depend on a dimension of dataVar; otherwise, if null, X-values are integer indices of the dimension of the dataVar, e.g. 0,1,...,length(dimensionX)-1. |
Variable varY | A 1d or 2d variable that contains Y-values. If not null, must depend on a dimension of dataVar; otherwise, if null, Y-values are integer indices of the dimension of the dataVar, e.g. 0,1,...,length(dimensionY)-1. |
VisualizationStyle style | Styles are described below. Here we say that this parameter must be presented in the constructor of your visualization and passed to the base class as is. |
4.
Add public properties to the visualization, such as stroke color, thickness, palette etc. Modification of a property must raise notification using
RaisePropertyChange() method. Add the
StyleProperty attribute to the properties which are allowed to be defined in the visual hints of the visualization. E.g. for this visualization we can use the visual hint
"y(x) Style:Polyline; Stroke:Blue".
[StyleProperty]
public string Stroke
{
set
{
Color = ColorHelper.ToColor(value); // Color is an inherited property of type Color
lineGraph.Stroke = Brush;
RaisePropertyChanged("Stroke");
}
get
{
return ColorHelper.ToString(Color);
}
}
The base class
DynamicDataDisplayVisualization has property
Color of type
Color. In the example above we override the
Color property with
Stroke property which converts
string into
Color and vice versa. Another inherited property,
Brush, just wraps the
Color as a
SolidColorBrush and returns the brush.
In general, the properties with the
StyleProperty attribute can be of any type; in particular it allows to define default value for the property, used when the property is missed in a visualization hint. Except default values, these properties are assigned by a corresponding
VisualizationStyle object which will be described below.
5.
Override the ViewType property. The property must return a type of the view which can host this visualization.
public override Type ViewType
{
get { return typeof(DynamicDataDisplayViewSingleAxis); }
}
Base class | View for the visualization |
DynamicDataDisplayVisualizationSingleAxis | DynamicDataDisplayViewSingleAxis |
DynamicDataDisplayVisualizationTwoAxes | DynamicDataDisplayViewTwoAxes, or GeoView for visualizations with background maps. |
DynamicDataDisplayPaletteVisualization | DynamicDataDisplayViewTwoAxes, or GeoView for visualizations with background maps. |
6.
Define a transformation of data into a visual representation in a view. Override the method
OnDataUpdated. It is invoked in the UI thread when a visualization is added to the view and then when the input data changes. Thus, the method implementation should either add new graph to the view or update existing; for this, a visualization may need to keep something as fields of the class, e.g. a graph instance of the D3 library.
Also note the way how data is taken: instead of direct use of variables passed in the constructor, variables to get data from
must be taken using
GetVariable method; it returns cached variable caused by concurrency issues.
protected override void OnDataUpdated()
{
if (!IsAttached) return;
// Creating new graph
if (lineGraph == null)
{
Variable cachedData = GetVariable(displayVar.ID);
Variable cachedAxis = GetVariable(axisVar.ID);
dataSource = ...
ShowPlot();
}
else // updating graph as new data received
{
dataSource.RaiseDataChanged();
}
IsRendered = true;
}
private void ShowPlot()
{
if (lineGraph == null)
lineGraph = new LineGraph(dataSource);
lineGraph.Stroke = Brush;
lineGraph.StrokeThickness = lineThickness;
if (!ChartPlotter.Children.Contains(lineGraph))
ChartPlotter.Children.Add(lineGraph);
NewLegend.SetDescription(lineGraph, DisplayedVariables[0].Metadata.GetDisplayName());
RaisePropertyChanged("Thumbnail");
}
7.
Set up appearance of the visualization in a visualization list. Override
CreateTopThumbnailContent so that it returns a control bound to the properties of the visualization. If need (e.g. for large palette thumbnail of heatmap), override
CreateBottomThumbnailContent method.
protected override object CreateTopThumbnailContent()
{
var line = new Line();
line.X1 = 2;
line.X2 = 14;
line.Y1 = 14;
line.Y2 = 2;
line.Stretch = Stretch.Fill;
line.DataContext = this;
Binding binding = new Binding("Stroke");
binding.Converter = new BrushFromStringConverter();
binding.Source = this;
line.SetBinding(Line.StrokeProperty, binding);
line.SetBinding(Line.StrokeThicknessProperty, "Thickness");
Canvas el = new Canvas();
el.Background = new SolidColorBrush(SystemColors.WindowColor);
el.Width = 15;
el.Height = 15;
el.Children.Add(line);
return el;
}
8.
Release resources when a visualization is removed from the view, i.e. detached. For this, override
Detach method:
public override void Detach(VisualView view)
{
if (lineGraph != null)
ChartPlotter.Children.Remove(lineGraph);
lineGraph = null;
base.Detach(view);
}
Visualization style and factory
The next steps create
a visualization style and
a factory for the style. A style is a type derived from the
VisualizationStyle class; it represents properties of a visualization and, when all properties are set properly, produces and updates the visualization instance itself. In our case, we should create a style for the visualization type with certain properties, so called "visualization properties". There is a class
SingleAxisStyle in the
DynamicDataDisplayViews.dll, which has predefined properties
X,Y, where
Y is a
VariablePropertyValue, and
X is either
VariablePropertyValue or
DimensionPropertyValue. You may derive your style class from the
SingleAxisStyle or from
VisualizationStyle.
9.
Create a class representing the visualization style. Also override
Style property which must return name of the style to be used in visual hints.
public sealed class PolylineStyle : SingleAxisStyle
{
private readonly Color defaultColor;
public PolylineStyle(DataSet dataset)
: base(dataset)
{
Thickness = new SliderPropertyValue(1.0) { TickFrequency = 0.5 };
Stroke = new ColorPropertyValue(defaultColor = DataSetViewerPalette.SharedInstance.GetNextColor());
}
public override string Style { get { return "Polyline"; } }
10.
Define visualization properties. A visualization property is a property of a style class, which has an attribute
VisualizationProperty and returns a value of type
VisualizationPropertyValue. The attribute allows to specify place of the property in visual hints (
HintPart); whether is accepts null as values (
AcceptsNull); whether the property's value constrains others (
AffectsOthers), e.g. "Color" is independent and "X" property of the polyline visualization affects set of allowed values for the "Y" properties, say, if "X" now depends on a dimension "i", then "Y" also must depend on this dimension.
The abstract
VisualizationPropertyValue class is a base class for all types property values; there are
VariablePropertyValue,
DimensionPropertyValue,
DoublePropertyValue,
SliderPropertyValue,
EnumPropertyValue,
ComboBoxEnumPropertyValue,
ColorPropertyValue,
PalettePropertyValue defined in the
DataSetViewerCore.dll, but also it is possible to create custom property values. The property values determines the visual representation of the property and possibly constrains the values; it may have associated value editor, e.g.
ColorPropertyValue is associated with the color picker control. This allows automatically create properties grid to show and modify their values. The properties are either dependency or notifiable properties so they could be used as binding sources.
When property changes, we should check whether new value is acceptable (regarding other properties of the style, too), and if it is, whether we should create and expose new visualization as the
Visualization property value for the new state of the style.
private VariablePropertyValue y;
private VisualiationPropertyValue x;
...
/// <summary>
/// Displayed line thickness.
/// </summary>
[VisualizationProperty("Thickness", AffectsOthers = false)]
public SliderPropertyValue Thickness
{
get { return (SliderPropertyValue)GetValue(ThicknessProperty); }
set { SetValue(ThicknessProperty, value); } // new value is propagated to visualization using binding
}
public static readonly DependencyProperty ThicknessProperty =
DependencyProperty.Register("Thickness", typeof(SliderPropertyValue), typeof(PolylineStyle),
new UIPropertyMetadata(null));
/// <summary>
/// Data variable of the visualization. (Already defined in the SingleAxisStyle class.)
/// </summary>
[VisualizationProperty(HintPart = HintPart.Data, AcceptsNull = true)]
public VariablePropertyValue Y
{
get { return y; }
set
{
if (!IsAcceptableAsY(value != null ? (Variable)value.Value : null))
throw new ArgumentException("Value of property Y is incorrect");
y = value;
RaisePropertyChanged("Y");
CheckVisualizationIsComplete(); // checks that visualization is completely defined and can be built
}
}
...
11.
Implement GetOptions method. The
VisualizationStyle exposes visualization properties and also provides information which options are allowed for those properties; when some of properties are set, allowed options for other may change. This allows step-by-step define all properties and when it is done,
VisualizationStyle produces a
Visualization instance, which is then also updated as some properties continue to change (and it may be reset to zero if some properties are unset). Since
DataSet class is used as a data source, it must be passed to the constructor as a parameter and allowed variables and dimensions are taken from that dataset.
public override VisualizationPropertyValue[] GetOptions(string propertyName)
{
if (String.IsNullOrEmpty(propertyName)) throw new ArgumentException("propertyName is undefined");
if (propertyName == "Thickness") return new VisualizationPropertyValue[] { Thickness };
if (propertyName == "Stroke") return new VisualizationPropertyValue[] { Stroke };
if (propertyName == "Y") return GetOptionsY();
if (propertyName == "X") return GetOptionsX();
throw new ArgumentException("There is no given property");
}
private VisualizationPropertyValue[] GetOptionsX()
{
if (y == null || y.Value == null)
{
var q = from v in dataset.Variables
where CheckGridVar(v)
select new VariablePropertyValue(v) as VisualizationPropertyValue;
q = q.Concat(Get1dDimensions(dataset).Select(dim => new DimensionPropertyValue(dim) as VisualizationPropertyValue));
return q.OrderByDescending(p => p, axisComparer).ToArray();
}
else
{
Variable varY = (Variable)y.Value;
var dim = varY.Dimensions[0];
var q = from v in dataset.Variables
where CheckGridVar(v) && varY != v && dim.Name == v.Dimensions[0].Name
select new VariablePropertyValue(v) as VisualizationPropertyValue;
q = q.Concat(new VisualizationPropertyValue[] { new DimensionPropertyValue(dim) as VisualizationPropertyValue });
return q.OrderByDescending(p => p, axisComparer).ToArray();
}
}
12.
Implement method to check whether the visualization style is compatible with the view, or not. This method can also consider proposed new property value (i.e. "what if this property is set to this value"). Override
IsCompatibleCore method;
if SingleAxisStyle is used as the base class, the method shouldn't be overriden in most cases.
protected override bool IsCompatibleCore(VisualView visualView, string propertyName,
VisualizationPropertyValue propertyValue)
{
...
}
13. If
SingleAxisStyle class is used as the base class, the
BuildVisualization,
ExposeVisualization, and
ClearBindings methods must be implemented. Otherwise, this should be checked and probably done when visualization properties are set.
protected override Visualization BuildVisualization() // builds the visualization instance when all properties are set
{
if (x is VariablePropertyValue)
return new LineGraphAxisVisualization((Variable)y.Value, (Variable)x.Value, false, this);
else
return new LineGraphDimensionVisualization((Variable)y.Value, false, this);
}
protected override void ExposeVisualization(Visualization vis) // binds and makes the vis instance visible from outside
{
((LineGraphVisualization)vis).Thickness = Thickness.DoubleValue;
Binding binding = new Binding("Thickness");
binding.Source = vis;
binding.Mode = BindingMode.TwoWay;
binding.Converter = new DoubleToSliderValueConverter();
binding.ConverterParameter = Thickness;
BindingOperations.SetBinding(this, ThicknessProperty, binding);
((LineGraphVisualization)vis).Stroke = ColorHelper.ToString(Stroke.Color);
binding = new Binding("Stroke");
binding.Source = vis;
binding.Mode = BindingMode.TwoWay;
binding.Converter = new StringToColorValueConverter();
binding.ConverterParameter = Stroke;
BindingOperations.SetBinding(this, StrokeProperty, binding);
Visualization = vis;
}
protected override void ClearBindings() // clears all bindings when the exposed visualization is released
{
var thickness = Thickness;
var stroke = Stroke;
BindingOperations.ClearBinding(this, ThicknessProperty);
BindingOperations.ClearBinding(this, StrokeProperty);
Thickness = thickness;
Stroke = stroke;
}
14.
Create a visualization style factory which is can produce instances of visualization style classes (each visualization style instance can produce only one visualization instance at a time). The factory class must implement
IPrimitiveVisualizationFactory (
DataSetViewerCore.dll) and has the MEF
Export attribute:
[Export(typeof(IPrimitiveVisualizationFactory))]
public sealed class PolylineFactory : IPrimitiveVisualizationFactory
{
public const string PolylineStyleStr = "Polyline";
public string Style
{
get { return PolylineStyleStr; }
}
public VisualizationStyle GetVisualizationStyle(DataSet dataset)
{
return new PolylineStyle(dataset);
}
}
Deployment
Put the resulting assembly into the
Visualizations subfolder of the DataSetViewer folder and run the viewer. The visualization will be available for appropriate dataset.