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.
  1. 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.
  2. 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).
  3. 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.
  4. 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.

Last edited Sep 12, 2011 at 1:28 PM by dvoits, version 47

Comments

No comments yet.