Visualize graph algorithms using MindFusion Diagram component

In this post we’ll explore visualization of graph processing algorithms using MindFusion.Diagramming API. The sample Visual Studio project will show animated depth-first and breadth-first search algorithms for graph traversal, but same approach can be applied for visualizing processes in any systems representable as graph data structures, such as message transmission in networks, progress of tasks in workflows, and so on. You can download the complete project here:

GraphSearch.zip

The code will show several techniques you might also find useful in other contexts:

  • build diagram programmatically from model data
  • use styles to temporarily apply several appearance attributes as a single unit
  • synchronize diagram with data coming from a worker thread

Let’s start by creating our (very simple) model classes, Graph and Vertex in this case, where connections in the graph will be stored using standard adjacency lists representation:

class Graph
{
	public List Vertices = new List();
}

class Vertex
{
	public List Neighbors = new List();
	public bool Visited;
	public int Index;
	public int SearchOrder;
}

Next, create a method that builds a diagram from the model objects. The mappings will be saved in a dictionary for later access.

private Dictionary<vertex, shapenode=""> nodes;
readonly RectangleF defaultSize = new RectangleF(0, 0, 10, 10);

///
/// Create diagram elements from graph with adjacency lists representation
/// 
void DiagramFromGraph(Graph g)
{
	diagram.ClearAll();

	// map graph vertices to diagram nodes
	nodes = new Dictionary<vertex, shapenode="">();

	// create a node for each vertex
	foreach (var v in g.Vertices)
	{
		var node = diagram.Factory.CreateShapeNode(defaultSize);
		node.Tag = v;
		nodes[v] = node;
	}

	// create links for adjacencies
	foreach (var v1 in g.Vertices)
	{
		foreach (var v2 in v1.Neighbors)
		{
			// only in one direction
			if (v1.Index < v2.Index)
				diagram.Factory.CreateDiagramLink(nodes[v1], nodes[v2]);
		}
	}

	// arrange the nodes
	new AnnealLayout { Randomize = false }.Arrange(diagram);

	// search starts from selected node
	diagram.Nodes[0].Selected = true;
}
</vertex,></vertex,>

Now create a sample graph and its corresponding drawing which we’ll use to show search progress:

void OnFormLoad(object sender, EventArgs e)
{
	// create sample graph to traverse
	var graph = new Graph();
	graph.GenerateRandom(20, 25);
	DiagramFromGraph(graph);
}

public void GenerateRandom(int v, int e)
{
    var rnd = new Random(42);
    for (int i = 0; i < v; i++)
        Vertices.Add(new Vertex { Index = i});
    int c = 0;
    while (e > 0)
    {
        var v1 = Vertices[c];
        var v2 = Vertices[rnd.Next(v)];
        if (v1 == v2 || v1.Neighbors.Contains(v2))
            continue;
        v1.Neighbors.Add(v2);
        v2.Neighbors.Add(v1);
        c = (c + 1) % v;
        e--;
    }
}

Add two styles we’ll use to show search progress. The first one is for vertices visited by the search algorithm, and the second one is applied temporarily when the algorithm back-tracks:

readonly ShapeNodeStyle visitedNodeStyle = new ShapeNodeStyle
   	{
   		Brush = new MindFusion.Drawing.SolidBrush(Color.Green)
   	};

readonly ShapeNodeStyle backtrackNodeStyle = new ShapeNodeStyle
	{
		Brush = new MindFusion.Drawing.SolidBrush(Color.DarkGreen),
		Stroke = new MindFusion.Drawing.SolidBrush(Color.Red),
		StrokeThickness = 1 // mm
	};

We’ll invoke the following methods from the search algorithm threads to show which vertices have just been processed:

void ShowProgress(Vertex v)
{
	// invoke in UI thread
	diagramView.Invoke(new System.Action(() =>
	{
		// update node style
		var node = nodes[v];
		node.Text = v.SearchOrder.ToString();
		node.Style = visitedNodeStyle;

		if (backtrackNode != null)
			backtrackNode.Style = visitedNodeStyle;
		backtrackNode = null;
	}));
	Thread.Sleep(animationDelay);
}

void ShowBacktrack(Vertex v)
{
	// invoke in UI thread
	diagramView.Invoke(new System.Action(() =>
	{
		if (backtrackNode != null)
			backtrackNode.Style = visitedNodeStyle;

		// update node style
		var node = nodes[v];
		node.Style = backtrackNodeStyle;
		backtrackNode = node;
	}));
	Thread.Sleep(animationDelay);
}

DiagramNode backtrackNode;
int animationDelay = 1000;

We now have everything ready for showing animated progress of graph algorithms. Add a form button that will run a sample depth-first search, add a click event handler called OnDepthFirstSearch, and handle it like this:

void OnDepthFirstSearch(object sender, EventArgs e)
{
	// do not search if there's no node selected
	var startNode = diagram.ActiveItem as DiagramNode;
	if (startNode == null)
		return;

	// search buttons disabled while current search thread runs
	btnDFS.Enabled = btnBFS.Enabled = false;

	// init data structures for new search
	ResetSearch();

	// get vertex corresponding to selected node
	var startVertex = (Vertex) startNode.Tag;

	// start depth-first search in a new thread
	currentSearch = new Thread(() =>
		DepthFirstSearch(startVertex, 0));
	currentSearch.Start();
}

int DepthFirstSearch(Vertex current, int order)
{
	// mark vertex as visited
	current.Visited = true;
	current.SearchOrder = order;

	// redraw its node from UI thread
	ShowProgress(current);

	// visit adjacent nodes
	foreach (var neighbor in current.Neighbors)
	{
		if (!neighbor.Visited)
		{
			// descend recursively
			order = DepthFirstSearch(neighbor, order + 1);

			// show in UI thread we are going back
			ShowBacktrack(current);
		}
	}

	if (current.SearchOrder == 0)
	{
		// enable search buttons
		SearchComplete();
	}

	return order;
}

Add a second button that will run breadth-first search thread:

private void OnBreadthFirstSearch(object sender, EventArgs e)
{
    // do not search if there's no node selected
    var startNode = diagram.ActiveItem as DiagramNode;
    if (startNode == null)
        return;

    // search buttons disabled while current search thread runs
    btnDFS.Enabled = btnBFS.Enabled = false;

    // init data structures for new search
    ResetSearch();

    // get vertex corresponding to selected node
    var startVertex = (Vertex)startNode.Tag;

    // start breadth-first search in a new thread
    currentSearch = new Thread(() =>
        BreadthFirstSearch(startVertex));
    currentSearch.Start();
}

void BreadthFirstSearch(Vertex start)
{
    int order = 0;

    // enqueue first vertex and mark as visited
    var queue = new Queue();
    queue.Enqueue(start);
    start.Visited = true;
    start.SearchOrder = order++;

    // while there are vertices in queue
    while (queue.Count > 0)
    {
        var current = queue.Dequeue();

        // show dequeued node
        ShowBacktrack(current);

        // add its neighbours to queue
        foreach (var neighbor in current.Neighbors)
        {
            if (!neighbor.Visited)
            {
                queue.Enqueue(neighbor);
                neighbor.Visited = true;
                neighbor.SearchOrder = order++;

                // show queued node
                ShowProgress(neighbor);
            }
        }
    }

    SearchComplete();
}

If you run the application now and click one of the search buttons, you should see this screen showing the algorithm progress, with current back-track position represented by a red border:

graph search visualized in mindfusion diagram control for .NET

The code above uses MindFusion’s .NET API and can be used with Windows Forms, WPF, Silverlight and ASP.NET diagramming components. The Java API for Android and desktop Swing application will look similar, with setter method calls instead of property assignments.

You can download the trial version of any MindFusion.Diagramming component from this page.

Enjoy!

Creating a proprietary invoice editor

In this post we will show how to create an invoice editing application (using MindFusion.Reporting) for the end users of an organization. The source code of the sample is available for download from here:

https://mindfusion.eu/_samples/ReportingInvoiceEditor.zip

Introduction
We start off by creating a new Windows Forms Application in Visual Studio 2010 or later. Change the target framework of the application to “.NET Framework 4” (or later). The ReportEditor component that will be used as an in-place invoice editor requires at least .NET 4.

Add the ReportEditor component to the main form, set its Dock to Fill.

The invoice template
The invoice template displayed by the application is stored in an XML file. The original template is created beforehand and is located in Invoice.xml. All modifications to the template done by the end users will be stored back to the XML file upon exiting the application. Add the following line to the main form’s constructor to load the invoice template when the main form is constructed:

reportEditor1.OpenReport(@"Invoice.xml");

Adding the data source
From the “Data -> Add New Data Source…” menu in Visual Studio create a new data source from the nwind.mdb database. Select the Orders table and the Invoices query in the Data Source Configuration Wizard. In the XML Schema (nwindDataSet.xsd) ensure that there is a relation between the Orders and Invoices table adapters. The relation should link the OrderID fields of the two tables and should be named “Orders_Invoices”. Build the application so that Visual Studio creates the classes for the data set and the selected table adapters. Go back to the main form designer and add nwindDataSet, InvoicesTableAdapter, and OrdersTableAdapter components to the form. In the constructor of the form, add the following lines in order to fill the data set with the data from the source database:

invoicesTableAdapter1.Fill(nwindDataSet1.Invoices);
ordersTableAdapter1.Fill(nwindDataSet1.Orders);

In addition, we need to register the two tables as data sources in the report editor. This is essential because these data sources are used by the invoice report. It is also important that the data sources are registered before the report is initially loaded through the OpenReport method.

reportEditor1.AddDataSource(nwindDataSet1.Orders, "Orders");
reportEditor1.AddDataSource(nwindDataSet1.Invoices, "Invoices");

Saving the template
Override the OnClosing event of the form and add the following line to ensure that all changes to the invoice template are written back to the XML file:

reportEditor1.SaveReport(@"Invoice.xml");

Adding the menu
Create a menu strip for the application with the following structure:

  • File
    • Print
    • Print Preview
    • Exit
  • Edit
    • Undo
    • Redo

Add the following event handlers for the menu items:

private void printToolStripMenuItem_Click(object sender, EventArgs e)
{
	var printer = new ReportPrinter();
	printer.Report = reportEditor1.Report;
	printer.Report.Run();
	printer.Print();
}

private void printPreviewToolStripMenuItem_Click(object sender, EventArgs e)
{
	var printer = new ReportPrinter();
	printer.Report = reportEditor1.Report;
	printer.Report.Run();

	var preview = new PrintPreviewForm();
	preview.Document = printer;
	preview.ShowDialog();
}

private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
	Close();
}

private void undoToolStripMenuItem_Click(object sender, EventArgs e)
{
	reportEditor1.Undo();
}

private void redoToolStripMenuItem_Click(object sender, EventArgs e)
{
	reportEditor1.Redo();
}

The image below illustrates the running application:

reporting-invoiceeditor

The MindFusion.Reporting component can be downloaded from here:

https://www.mindfusion.eu/ReportingTrial.zip

Enjoy!

Database schema diagram

In this post we’ll show how to use TableNode objects to display tabular data, more specifically database schema information. A Visual Studio sample project containing the code from this post is available for download here:

DatabaseSchema.zip

To start, create a new Windows Forms application, and place a text field for connection string, a button and a DiagramView on the form. In the code-behind file, add following field to map table name to respective TableNode objects:

Dictionary<string, tablenode=""> tables = new Dictionary<string, tablenode="">();
</string,></string,>

Add a RectangleF that stores default size passed to CreateTableNode method:

RectangleF defaultSize = new RectangleF(0, 0, 30, 30);

Create a ReadTables method, which provided an SqlConnection, parses its schema information and creates diagram nodes:

void ReadTables(SqlConnection connection)
{
	// get table schema definitions from connection
	var schema = connection.GetSchema("Tables");
	foreach (DataRow row in schema.Rows)
	{
		// fetch table name
		var name = row["TABLE_NAME"].ToString();

		// create respective node
		var table = diagram.Factory.CreateTableNode(defaultSize);
		table.Caption = name;
		table.Shape = SimpleShape.RoundedRectangle;
		table.Brush = new MindFusion.Drawing.SolidBrush(Color.LightGray);

		// register node in dictionary for future foreign key reference
		tables[name.Replace(" ", "_")] = table;
		ReadFields(table, connection,
			row["TABLE_CATALOG"].ToString(), null, name);
	}

	ReadForeignKeys(connection);
}

The ReadFields method takes table node and name parameters and creates node cells that will show information for the column name and type of database tables:

void ReadFields(TableNode node,
	SqlConnection connection, string db, string owner, string tableName)
{
	// remove default cells
	node.RowCount = 0;

	// reserve one column for name and one for data type
	node.ColumnCount = 2;

	// read column definitions of specified table
	var schema = connection.GetSchema("Columns", new[] { db, owner, tableName });
	foreach (DataRow row in schema.Rows)
	{
		// add a new row to the node
		int r = node.AddRow();

		// set cells' text to the column name and type
		node[0, r].Text = row["COLUMN_NAME"].ToString();
		node[1, r].Text = row["DATA_TYPE"].ToString();

	}

	// make table cells big enough to show all text
	node.ResizeToFitText(false);
}

The ReadForeignKeys method creates DiagramLink connectors between table nodes to show the relationships between database tables:

void ReadForeignKeys(SqlConnection connection)
{
	var schema = connection.GetSchema("ForeignKeys");
	foreach (DataRow row in schema.Rows)
	{
		// read foreign key information
		string fkName = row["CONSTRAINT_NAME"].ToString();
		string tableName = row["TABLE_NAME"].ToString().Replace(" ", "_");
		string prefix = "FK_" + tableName + "_";
		if (fkName.StartsWith(prefix))
		{
			string targetName = fkName.Substring(prefix.Length);

			// get table nodes registered for specified names
			if (tables.ContainsKey(targetName) && tables.ContainsKey(tableName))
			{
				var table = tables[tableName];
				var targetTable = tables[targetName];

				// create a link between the nodes to show relationship
				diagram.Factory.CreateDiagramLink(table, targetTable);
			}
		}
	}
}

Finally handle the button’s click event to open specified connection and call ReadTables. Apply AnnealLayout to arrange the tables so that they do not overlap:

private void btnOpen_Click(object sender, System.EventArgs e)
{
	diagram.ClearAll();

	try
	{
		var connection = new SqlConnection(tbConnection.Text);
		connection.Open();

		// read schema and create corresponding diagram items
		ReadTables(connection);

		connection.Close();
	}
	catch (Exception exception)
	{
		MessageBox.Show(exception.Message);
		diagram.ClearAll();
	}

	// arrange the tables to remove overlaps
	var layout = new AnnealLayout();
	layout.SplitGraph = true;
	layout.Randomize = false;
	layout.MultipleGraphsPlacement = MultipleGraphsPlacement.MinimalArea;
	layout.Margins = new SizeF(10, 10);
	layout.Arrange(diagram);
}

If you run the project and open the Northwind sample database by Microsoft, you should see this diagram:

database schema layout

The code above uses MindFusion’s .NET API and can be used with Windows Forms, WPF, Silverlight and ASP.NET diagramming components. The Java API for Android and desktop Swing application will look similar, with setter method calls instead of property assignments.

You can download the trial version of any MindFusion.Diagramming component from this page.

Enjoy!

MindFusion.Gauges: Creating a Stopwatch

This blog will demonstrate how to create a simple stopwatch based on the OvalGauge component (part of MindFusion.Charting for WinForms and MindFusion.Pack for WinForms).

Introduction
We start off by creating a new Windows Forms Application in Visual Studio and adding an OvalGauge control to the main form. Set the gauge’s Dock property to Fill and its Name to “stopwatch”.

Setting up the scales
The stopwatch will contain two scales – a bigger one, which should display the seconds, and a smaller one – for the minutes. The minute scale will be positioned in the upper half of the gauge area. The scales can be created and configured in design-time – through the property grid – or in code. In the Property grid navigate to the Scales property of the OvalGauge control and click the ‘…’ button or hit F4 to open the OvalScale Collection Editor. In this editor, add two scales by pressing the ‘Add’ button. Edit the following properties of the second scale:

Name = "SecondsScale"
StartAngle = -90
EndAngle = 270
StartWidth = 0
EndWidth = 0
Stroke = Color=Black, Width=0

MinorTickSettings.ShowTicks = True
MinorTickSettings.Count = 4
MinorTickSettings.Stroke = Color=(85, 85, 85), Width=0
MinorTickSettings.TickShape = Line
MinorTickSettings.TickWidth = 5%
MinorTickSettings.TickAlignment = OuterInside

MiddleTickSettings.ShowLabels = False
MiddleTickSettings.Count = 5
MiddleTickSettings.Stroke = Color=(0, 0, 0), Width=0
MiddleTickSettings.TickShape = Line
MiddleTickSettings.TickWidth = 8%
MiddleTickSettings.TickAlignment = OuterInside

MajorTickSettings.Count = 12
MajorTickSettings.FontFamily = "Calibri"
MajorTickSettings.FontSize = 20%
MajorTickSettings.FontStyle = Regular
MajorTickSettings.LabelOffset = 9%
MajorTickSettings.LabelRotation = None
MajorTickSettings.ShowMaxValueTick = Never
MajorTickSettings.Stroke = Color=(0, 0, 0), Width=2
MajorTickSettings.TickShape = Line
MajorTickSettings.TickWidth = 12%
MajorTickSettings.TickAlignment = OuterInside

In addition, add four custom intervals to the MajorTickSettings.CustomIntervals collection, representing the intervals [0-0], [15-15], [30-30], and [45-45] respectively. Set the Fill property of each custom interval to (187, 31, 33). These objects indicate that the labels inside the corresponding intervals will be colored in red.

For simplicity, the properties of the minute scale are omitted. They are similar to those of the second scale, the main difference being that the minute scale is positioned and sized through the ScaleRelativeCenter and ScaleRelativeRadius properties.

Adding pointers

In the OvalScale Collection Editor dialog add a pointer to each of the two scales. To do this, select the scale, navigate to the Pointers property in the grid and press the ‘…’ button or F4. This will open the Pointer Collection Editor dialog. Name the pointer of the minute scale “MinutesPointer” and the one in the second scale – “SecondsPointer”. We will use these names later in code to identify and search for the pointers. Set the width of the two pointers to 30% and 20% respectively. Then close all editors. We will custom draw the pointers so there is no need to specify any more properties.

To draw the pointers, handle the PrepaintPointer event of the OvalGauge class. This event is raised for each pointer in the gauge individually before the pointer is painted by the control. The event can be used to cancel the default pointer drawing by setting the CancelDefaultPainting to true. A simplified version of the event handler that paints the two pointers is listed below.

private void stopwatch_PrepaintPointer(object sender, PrepaintEventArgs e)
{
    e.CancelDefaultPainting = true;

    Brush fill = new SolidBrush(Color.FromArgb(175, 8, 9));

    GraphicsState state = e.Graphics.Save();
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.ScaleTransform(e.Element.RenderSize.Width, e.Element.RenderSize.Height);

    if (e.Element.Name == "SecondsPointer")
    {
         var polygon = new PointF[]
         {
             new PointF(0.1f, 0.35f),
             new PointF(1, 0.45f),
             new PointF(1, 0.55f),
             new PointF(0.1f, 0.65f)
         };
         var ellipse1 = new RectangleF(0, 0, 0.2f, 1f);

        // Draw the pointer itself
        e.Graphics.FillPolygon(fill, polygon);
        e.Graphics.FillEllipse(fill, ellipse1);
    }
    else if (e.Element.Name == "MinutesPointer")
    {
        var polygon = new PointF[]
        {
            new PointF(0.15f, 0.35f),
            new PointF(1, 0.45f),
            new PointF(1, 0.55f),
            new PointF(0.15f, 0.65f)
        };
        var ellipse1 = new RectangleF(0, 0, 0.3f, 1f);

        e.Graphics.FillPolygon(fill, polygon);
        e.Graphics.FillEllipse(fill, ellipse1);
    }

    e.Graphics.Restore(state);
    fill.Dispose();
}

Activating the stopwatch

Now we need means to start, stop, and reset the stopwatch. First, declare two member variables in the form – startTime and elapsedTime, of type DateTime and TimeSpan respectively. The first variable will store the time when the stopwatch was most recently started. The second variable will store the total elapsed time.

Go to the Visual Studio designer, select the form, navigate to the Padding property and change its value to “0, 0, 0, 50”. The gauge will shrink, leaving some space at the bottom. Add two buttons to the form, below the gauge, and set their texts to “Start” and “Reset” respectively. In addition to the buttons, add a Timer component to the form. When the stopwatch is started, we will use this timer to update the pointers. Handle the Click events of the two buttons. The first button will start/stop the stopwatch. The second button will reset the stopwatch. Here is the event handlers of the two buttons:

private void button1_Click(object sender, EventArgs e)
{
    if (timer1.Enabled)
    {
        UpdateTime();
        UpdatePointers();

        timer1.Stop();
        button1.Text = "Start";
    }
    else
    {
        button1.Text = "Stop";
        startTime = DateTime.Now;
        timer1.Start();
    }
}

private void button3_Click(object sender, EventArgs e)
{
   elapsedTime = TimeSpan.Zero;
   UpdatePointers();
}

Depending on the current state of the application, the first button will either start or stop the timer. The second button will set the elapsed time to 0:00, effectively resetting the stopwatch. Both buttons rely on the method UpdatePointers, which adjusts the gauge pointers according to the currently elapsed time. This method uses the GetElementByName method of the gauge to access the pointers by name and update their values:

private void UpdatePointers()
{
    var minutesPointer = (Pointer)stopwatch.GetElementByName("MinutesPointer");
    var secondsPointer = (Pointer)stopwatch.GetElementByName("SecondsPointer");
    minutesPointer.Value = (float)elapsedTime.TotalMinutes;
    secondsPointer.Value = (float)Math.Round((float)(elapsedTime.TotalSeconds % 60), 1);
}

Finally, handle the Tick event of the timer.

private void timer1_Tick(object sender, EventArgs e)
{
    UpdateTime();
    UpdatePointers();
}

The image below illustrates the running stopwatch:

gauge-stopwatch

The source code of the sample is available for download from here:
https://mindfusion.eu/_samples/GaugesStopwatch.zip

The MindFusion.Charting for WinForms component can be downloaded from here:
https://www.mindfusion.eu/ChartWinFormsTrial.zip

About MindFusion.Charting for WinForms: A professional programming component for WinForms, which lets you create remarkable charts and gauges fast and easy. The tool supports all major chart types – line, pie, radar and bar – and numerous variations of them – column, area, bubble, polar, doughnut etc, as well as oval and linear gauges.

Real-time Chart: Amplification Plot

MindFusion.RealTimeCharting for WPF assembly lets you visualize huge (and by that we mean huge) amounts of data with no special load on the machine it runs on. We drew inspiration from a popular chart in molecular biology – Real-time PCR, which:

can provide a simple and elegant method for determining the amount of a target sequence or gene that is present in a sample.

More on the topic here.

Since we self-generate our data, the result graphics are not that much the real thing but they demonstrate the algorithm of building the chart pretty well. Let’s start:

1. Create the chart

Drag the RealTimeChart from the toolbox or add a reference to the MindFusion.RealTimeCharting.Wpf assembly. The chart has no initial data so we start with a simple X-axis on the screen. We name our chart “rtChart”.

2. Customizing the X-axis

The X-axis is accessible via the rtChart.XAxis property. It exposes many appearance settings, from which we start with Interval, Length and Origin – they will define our axis. When data accumulates, the initial axis labels shall be replaced with their updated values.

   rtChart.XAxis.Title = "Cycle";
   rtChart.XAxis.Interval = 2;
   rtChart.XAxis.Length = 50;
   rtChart.XAxis.Origin = 0;
   rtChart.XAxis.LabelFormat = "0";
   rtChart.XAxis.PinLabels = false;

It’s worth noting that we don’t pin labels e.g. they will move along the axis as new data is added. We need no trailing zeros, so the LabelFormat is “0”.

3. The Y-axes

There is no limit on how many Y-axis we will create. In our sample we will use two: one at each side of the chart plot area. We need two instances of the Axis class, which we add to YAxisCollection and Y2AxisCollection respectively. Before we add them, we set their appearance. We want tick marks and rotated title. Note that we align the second axis (Y2) to the right.

   Axis yAxis = new Axis();
   yAxis.Origin = 0.0;
   yAxis.Length = 1;
   yAxis.Interval = 0.1;
   yAxis.Title = "ΔRn";
   yAxis.TitleRotationAngle = -90.0;
   yAxis.TitleFontFamily = new FontFamily("Verdana");
   yAxis.LabelFontFamily = new FontFamily("Verdana");
   yAxis.TickLength = 5;
   yAxis.TitleOffset = 10;
   rtChart.YAxisCollection.Add(yAxis);          

   yAxis = new Axis();
   yAxis.Origin = 0.0;
   yAxis.Length = 100;
   yAxis.Interval = 10;
   yAxis.Title = "Fluorescene";
   yAxis.TitleRotationAngle = -90.0;
   yAxis.TitleFontFamily = new FontFamily("Verdana");
   yAxis.LabelFontFamily = new FontFamily("Verdana");
   yAxis.TickLength = 5;
   yAxis.TitleOffset = 10;
   yAxis.LabelHorizontalAlignment = HorizontalAlignment.Right;
   rtChart.Y2AxisCollection.Add(yAxis);

4. Chart series

We need four series – two of them will be bound to Y and two – to Y2. It’s important to note that axes must be initialized before the series, because each series is associated with a given Y-axis when created.

Each chart series is an instance of the Series class. We want to show scatters at each series – for that we use the ScatterType property. Since we plan to show a legend, it’s important to set the Title of each series – because this title will be used as a legend label. Here is sample code for the first series:

  series1 = new Series(rtChart.YAxisCollection[0])
  {
      Stroke = new SolidColorBrush(Colors.Green),
      ScatterStroke = new SolidColorBrush(Colors.Green),
      Fill = new SolidColorBrush(Colors.Green),
      ScatterType = ScatterType.Diamond,
      Title = "Sample 1",
      TitleFontFamily = new FontFamily("Verdana"),
      TitleFontSize = 12

   };

    .......
   rtChart.SeriesCollection.Add(series1);

5. Data

Data is added directly to the Series.Data property. You can add a batch of points or one point at a time. The property requires that you add a Point e.g. you must set the X and Y values simultaneously:

Point[] points1 = new Point[clusterSize];

//generate some dummy data
...

series1.Data.AddRange(points1);
rtChart.Commit(minNewX);

Don’t forget to call the Commit() method in one of its overloads – it signals to the chart that new data has been added and refresh is needed.

6. Final adjustments

We want a legend and we turn on ShowLegend. The initial result is a legend in 4 rows, which does not look beautifully on our chart. We plat for a while with LegendWidth and LegendHeight and come up with a satisfactory outlook for our legend – in two columns, centered below the plot area.

Adding a tooltip is also easy – we set ShowFallbackTooltip to true. Since the chart might have numerous Y-axis we must choose, which axis the tooltip is bound to. In our case it’s the first one:

    rtChart.TooltipVisibility = Visibility.Visible;
    rtChart.ShowFallbackTooltip = true;
    rtChart.TooltipAxis = rtChart.YAxisCollection[0];

    rtChart.ShowLegend = true;
    rtChart.LegendHeight = 70;

We adjust the grid according to the data we have:

    rtChart.MajorGridSizeY = 50;
    rtChart.MajorGridSizeX = 2;

7. That’s it

Here is the final result:

Real-time chart: amplification plot sample.

Real-time chart: amplification plot sample.

Of course, there are many other settings and possibilities in the control. You can download the sample and expand its functionality and appearance:

Real-time Chart – Amplification Plot Sample Download

MindFusion.RealTimeCharting for WPF is part of MindFusion.Charting for WPF component, which also includes MindFusion.Gauges for WPF. Check the trial version for more practical, beautiful and easy to build charts and gauges.

About MindFusion.RealTimeCharting for WPF: A WPF programming component, which has been designed and developed to render real-time charts with huge amounts of data in a fast and efficient manner. The component uses innovative approach to draw the chart graphics, which forgoes the traditional constructing of a tree with the visual elements in WPF. This way CPU load remains minimal and graphics of tens of thousands of points are rendered with impressive speed. The tool supports unlimited number of Y and Y2 axes, legend, tooltip, background image, grid and more.

About MindFusion.Charting for Wpf: A programming component that combines powerful charting capabilities with an elegant API and easy use. Among the features of the control are fully customizable grid, positive and negative values on all chart axes, 3D charts, gauges and many more – read a detailed list here.

The control provides detailed documentation and various samples that demonstrate how to customize every type of chart. It supports a wide range of 2D and 3D charts including bar, line, radar, bubble pie etc. You can add tooltips, define themes, perform hit testing, zoom and more.