Formula shapes with adjustable parameters in Flowchart.NET

In this post we’ll show how to create custom formula shapes with adjustable control points. Shape control points are a new feature in version 6.1 of the Flowchart.NET control, which is currently in beta tests. You can download a copy of the beta version from the following link.

https://mindfusion.eu/_beta/DiagWinForms61.zip

A shape formula is defined using a script, which calls one of the following functions to draw the node shape.

MoveTo (x,y) Moves the current position to the specified point without drawing.
LineTo (x,y) Draws a line from the current position to the specified point.
BezierTo (x1,y1,x2,y2,x3,y3) Draws a Bezier curve from the current position to (x3,y3) using (x1,y1) and (x2,y2) as control points.
ArcTo (x,y,largeArc,clockwiseArc,rx,ry) Draws an arc from the specified point to (x,y) where rx and ry are the ellipse radiuses and the arc flags are boolean values specifying which of the four possible arcs to draw.

For example, the following formula defines a rounded rectangle shape using lines and arcs, and expects to receive a “radius” control point parameter that will control the corner radii:

// a rounded rectangle shape, with an arc at each corner
string roundRect = @"
	r = Min(Width / 2, radius.X);
	MoveTo(r, 0);
	LineTo(Width - r, 0);
	ArcTo(Width, r, false, false, r, r);
	LineTo(Width, Height - r);
	ArcTo(Width - r, Height, false, false, r, r);
	LineTo(r, Height);
	ArcTo(0, Height - r, false, false, r, r);
	LineTo(0, r);
	ArcTo(r, 0, false, false, r, r);
 ";

When creating a Shape instance, we must add a ShapeControlPoint object to it that defines the radius parameter and its constraints. The following code specifies that the default radius is 5, the minimum and maximum values allowed are 1 and 15 respectively, and prevents the control point from moving vertically by setting minY and maxY to 0.

var myRect = new Shape(roundRect, "MyRect");

// add a control point for the 'radius' parameter
myRect.ControlPoints.Add(new ShapeControlPoint(
	"radius", 5, 1, 15, UnitType.Fixed, 0, 0, 0, UnitType.Fixed));

Here is another example that defines an anchor-like shape with two parameters controlling the tips of the anchor arms.

// an anchor shape, with two arcs outlining each anchor arm
string anchor = @"
	r = Width / 3;
	y1 = p1.Y * Height / 100;
	y2 = p2.Y * Height / 100;
	MoveTo(Width / 2, Height);
	MoveTo(Width / 2 + 3, Height - 5);
	ArcTo(Width, y2, false, true, r, r);
	ArcTo(Width / 2 + 3, Height - 10, false, false, r, r);
	LineTo(Width / 2 + 3, 0);
	LineTo(Width / 2 - 3, 0);
	LineTo(Width / 2 - 3, Height - 10);
	ArcTo(0, y1, false, false, r, r);
	ArcTo(Width / 2 - 3, Height - 5, false, true, r, r);
	LineTo(Width / 2, Height);
 ";

var myAnchor = new Shape(anchor, "MyAnchor");

// add control points at the tips of anchor arms
myAnchor.ControlPoints.Add(new ShapeControlPoint(
	"p1", 0, 0, 0, UnitType.Percentage, 55, 50, 80, UnitType.Percentage));
myAnchor.ControlPoints.Add(new ShapeControlPoint(
	"p2", 100, 100, 100, UnitType.Percentage, 55, 50, 80, UnitType.Percentage));

The following diagram contains several nodes displaying the shapes above, with some of the control points moved to different positions.

Some additional functions that you can call from shape scripts are listed below.

PI() Returns the value of PI.
Abs(x) Returns the absolute value of x.
Atn(x) Returns the angle, measured in radians, whose tangent is the specified number.
Cos(x) Returns the cosine of the specified angle.
Acos(x) Returns the angle whose cosine is the specified number.
Exp(x) Returns e raised to the specified power.
Log(x) Returns the natural (base e) logarithm of the specified value.
Pow(x,power) Returns a specified number raised to the specified power.
Sin(x) Returns the sine of the specified angle.
Asin(x) Returns the angle whose sine is the specified number.
Sqrt(x) Returns the square root of a number.
Tan(x) Returns the tangent of the specified angle.
Min(x,y) Returns the smaller of two numbers.
Max(x,y) Returns the larger of two numbers.

The complete sample project is available for download here:
https://mindfusion.eu/_samples/ParamShapes.zip

Enjoy!

Create diagram connectors using drag and drop

In this post we show how to create diagram links via drag-and-drop operation from a NodeListView used as a palette. Since the NodeListView control can contain only DiagramNode objects, we will create a custom node shape representing a connector. Once the dummy connector node is created, the NodeCreated event handler will replace it with a DiagramLink. In addition, the handler will find nearby nodes and automatically connect the link to them.

Let’s start with a new WPF project. Add a reference to mindfusion.diagramming.dll, and add Diagram and NodeListView controls to the window:

<ScrollViewer
	Grid.Column="0"
	Focusable="False"
	HorizontalScrollBarVisibility="Visible">
	
	<diag:Diagram
		x:Name="diagram"
		AllowDrop="True"
		NodeCreated="OnNodeCreated"
		NodeModified="OnNodeModified">
	</diag:Diagram>
	
</ScrollViewer>

<diag:NodeListView
	x:Name="nodeList"
	Grid.Column="1">
</diag:NodeListView>

In the Window constructor, create a custom node shape that will represent connectors:

var connectorShape = new Shape(
	null, // no borders
	new[] // decorations
	{
		new LineTemplate(10, 10, 10, 50),
		new LineTemplate(10, 50, 90, 50),
		new LineTemplate(90, 50, 90, 90)
	},
	null,
	FillRule.Nonzero, "Connector");

Add a few nodes to the NodeListView, along with the dummy connector node:

var item1 = new ShapeNode { Shape = Shapes.Rectangle };
NodeListView.SetLabel(item1, "Activity");
nodeList.Items.Add(item1);

var item2 = new ShapeNode { Shape = Shapes.Decision };
NodeListView.SetLabel(item2, "Decision");
nodeList.Items.Add(item2);

var item3 = new ShapeNode { Shape = connectorShape };
NodeListView.SetLabel(item3, "Connector");
nodeList.Items.Add(item3);

In the NodeCreated event handler, check if the newly created node represents a connector, and replace it with a DiagramLink:

var node = e.Node as ShapeNode;
if (node != null)
{
	if (node.Shape.Id == "Connector")
	{
		// replace the dummy connector node with a DiagramLink
		var bounds = node.Bounds;
		diagram.Items.Remove(node);

		var link = diagram.Factory.CreateDiagramLink(
			bounds.TopLeft, bounds.BottomRight);
		link.SegmentCount = 3;
		link.Shape = LinkShape.Cascading;

		ConnectToNearbyNode(link);
	}
	...
}

Otherwise if it is a regular node, set its anchor points and connect it to nearby unconnected links, if there are any:

else
{
	node.AnchorPattern = AnchorPattern.Decision2In2Out;
	node.Effects.Add(new GlassEffect());
	ConnectToNearbyLink(node);
}

The ConnectToNearbyNode method uses a LINQ query to find nodes in the vicinity of the link’s start or end point. If one is found at a distance shorter than 120 points, it is set as the link’s Origin or Destination.

private void ConnectToNearbyNode(DiagramLink link)
{
	// connect to a nearby origin node
	var origin = diagram.Nodes.OrderBy(n =>
		Utilities.Distance(n.GetCenter(), link.StartPoint)).FirstOrDefault();

	if (origin != null)
	{
		var distance = Utilities.Distance(origin.GetCenter(), link.StartPoint);
		if (distance < 120)
		{
			link.Origin = origin;
			link.Route();
		}
	}

	// connect to a nearby destination node
	var destination = diagram.Nodes.Where(n => n != origin).OrderBy(n =>
		Utilities.Distance(n.GetCenter(), link.EndPoint)).FirstOrDefault();

	if (destination != null)
	{
		var distance = Utilities.Distance(destination.GetCenter(), link.EndPoint);
		if (distance < 120)
		{
			link.Destination = destination;
			link.Route();
		}
	}
}

Similarly, the ConnectToNearbyLink method finds a nearby unconnected link and sets the specified node as the link’s Origin or Destination.

private void ConnectToNearbyLink(DiagramNode node)
{
	var outLink = diagram.Links.Where(l => l.Origin is DummyNode).OrderBy(l =>
		Utilities.Distance(node.GetCenter(), l.StartPoint)).FirstOrDefault();

	if (outLink != null)
	{
		var distance = Utilities.Distance(node.GetCenter(), outLink.StartPoint);
		if (distance < 90)
		{
			outLink.Origin = node;
			outLink.Route();
			return;
		}
	}

	var inLink = diagram.Links.Where(l => l.Destination is DummyNode).OrderBy(l =>
		Utilities.Distance(node.GetCenter(), l.EndPoint)).FirstOrDefault();

	if (inLink != null)
	{
		var distance = Utilities.Distance(node.GetCenter(), inLink.EndPoint);
		if (distance < 90)
		{
			inLink.Destination = node;
			inLink.Route();
			return;
		}
	}
}

The result of several drag and drop operation is displayed below.

The complete sample project is available for download here:
https://mindfusion.eu/_samples/LinkDragDrop.zip

Enjoy!

PERT network charts in WPF

In this post we show how to build a PERT network chart using MindFusion.Diagramming for WPF.

First, create a new WPF project and add a mindfusion.diagramming.wpf.dll reference. Add the appropriate Xaml namespace to the window Xaml file:

xmlns:diag="http://mindfusion.eu/diagramming/wpf"

Add a ScrollViewer and Diagram to the window markup:

<scrollviewer>
	<diag:diagram x:name="diagram">
</diag:diagram></scrollviewer>

There are two conventions for drawing PERT networks: activity on arrow and activity on node. Activity on arrow shows activity information on arrows, and the nodes they connect represent project milestones. Activity on node shows activity information on nodes and the arrows between them display relationships. We will use the first convention.

Let’s define a model class to hold activity data:

public class Activity
{
	// activity name
	public string Name { get; set; }

	// start milestone name
	public string From { get; set; }

	// end milestone  name
	public string To { get; set; }

	// time in months
	public int Time { get; set; }
}

Define a simple PERT project consisting of five milestones and six activities:

var network = new[]
{
	new Activity { Name = "A", From = "10", To = "30", Time = 3 },
	new Activity { Name = "B", From = "10", To = "20", Time = 4 },
	new Activity { Name = "C", From = "20", To = "50", Time = 3 },
	new Activity { Name = "D", From = "30", To = "40", Time = 1 },
	new Activity { Name = "E", From = "30", To = "50", Time = 3 },
	new Activity { Name = "F", From = "40", To = "50", Time = 3 }
};

Create a diagram node for each milestone in the project:

// create diagram nodes for each milestone
foreach (var activity in network)
{
	if (diagram.FindNodeById(activity.From) == null)
	{
		var node = diagram.Factory.CreateShapeNode(bounds, Shapes.Ellipse);
		node.Id = node.Text = activity.From;
	}

	if (diagram.FindNodeById(activity.To) == null)
	{
		var node = diagram.Factory.CreateShapeNode(bounds, Shapes.Ellipse);
		node.Id = node.Text = activity.To;
	}
}

Create a diagram link for each activity and display the activity name and estimated time as link labels:

// create a diagram link for each activity
foreach (var activity in network)
{
	var link = diagram.Factory.CreateDiagramLink(
		diagram.FindNodeById(activity.From),
		diagram.FindNodeById(activity.To));

	var label1 = link.AddLabel(activity.Name);
	label1.RelativeTo = RelativeToLink.Segment;
	label1.Index = 0;
	label1.VerticalOffset = -20;
	label1.TextBrush = new SolidColorBrush(Colors.Red);

	var label2 = link.AddLabel(string.Format("t={0}mo", activity.Time));
	label2.RelativeTo = RelativeToLink.Segment;
	label2.Index = 0;
	label2.VerticalOffset = 10;
	label2.TextBrush = new SolidColorBrush(Colors.Blue);
}

Use automatic graph layout algorithm to arrange the PERT network chart:

// arrange the diagram from left to right
var layout = new LayeredLayout();
layout.Margins = new Size(100, 100);
layout.EnforceLinkFlow = true;
layout.Orientation = Orientation.Horizontal;
layout.Arrange(diagram);

The final result is displayed below.
PERT network chart

For more information on the PERT methodology, see this Wikipedia article:
http://en.wikipedia.org/wiki/PERT

The complete sample project is available for download here:
https://mindfusion.eu/_samples/PertNetwork.zip

All MindFusion.Diagramming libraries expose the same programming interface, so most of the sample code shown above will work with only a few modifications in Windows Forms, ASP.NET, Silverlight and Java versions of the control.

Enjoy!

Create a block diagram editor in JavaScript

In this example, we will create a block diagram editor using MindFusion.Diagramming for JavaScript.

First, let’s add two HTML canvas elements to a page, one for the Diagram control and one for a NodeListView control that will serve as a tool palette:

<!-- The Diagram component is bound to the canvas element below -->

<div style="width:800px; height:600px; overflow:auto; border: 1px solid;">
  <canvas id="diagram" width="2100" height="2100">
    This page requires a browser that supports HTML 5 Canvas element.
  </canvas>
</div>


<!-- NodeListView control -->

<div style="width:130px; height:600px; overflow:none; border: 1px solid rgb(0, 0, 0);">
  <canvas id="nodeList" width="130" height="600"></canvas>
</div>

Next, add references to the necessary script files:

<script src="MicrosoftAjax.js" type="text/javascript"></script>
<script src="MindFusion.Diagramming.js" type="text/javascript"></script>
<script src="BlockDiagEdit.js" type="text/javascript"></script>

In the load handler in the script file, setup the diagram properties and populate the node list with prototype containers and shape nodes, that will represent respectively systems and modules in our block diagrams:

Sys.Application.add_load(function (sender, args)
{
	// create a Diagram component that wraps the "diagram" canvas
	diagram = $create(Diagram, null, null, null, $get("diagram"));
	diagram.addEventListener(Events.nodeCreated, onNodeCreated);
	diagram.addEventListener(Events.linkCreated, onLinkCreated);
	diagram.setLinkHeadShapeSize(3);
	diagram.setLinkHeadShape("Alternative");
	diagram.setAllowInplaceEdit(true);

	// create a NodeListView component that wraps the "nodeList" canvas
	var nodeList = $create(MindFusion.Diagramming.NodeListView,
        null, null, null, $get("nodeList"));
	nodeList.setIconSize(new Size(48, 48));
	nodeList.setDefaultNodeSize(new Size(24, 24));

	var colors = [
		"#FF5555",
		"#55FF55",
		"#5555FF",
		"#FFFFFF"
	];

	// add container to the NodeListView to represent "system" blocks
	for (var i = 0; i < 4; ++i)
	{
		var node = new ContainerNode(diagram);
		node.setBrush(colors[i]);
		nodeList.addNode(node, "System " + (i + 1));
	}

	// add container to the NodeListView to represent "module" blocks
	for (var i = 0; i < 4; ++i)
	{
		var node = new MindFusion.Diagramming.ShapeNode(diagram);
		node.setShape("Rectangle");
		node.setBrush(colors[i]);
		nodeList.addNode(node, "Module " + (i + 1));
	}
});

Handle the nodeCreated event to make containers larger, and start inplace edit operation to let users enter text immediately after dropping a node:

function onNodeCreated(sender, args)
{
	var node = args.getNode();
	if (ContainerNode.isInstanceOfType(node))
	{
		// make containers larger
		var bounds = node.getBounds().clone();
		bounds.width = 100;
		bounds.height = 75;
		node.setBounds(bounds);
	}

	// let user enter text immediately
	diagram.beginEdit(node);
}

When there aren’t anchor points defined, the diagram control snaps link points to nearest point of nodes borders. Let’s automatically align points when links are created to make them straight horizontal or vertical lines if they are already close to being such. Otherwise leave points unchanged to let users draw diagonal lines too:

function onLinkCreated(sender, args)
{
	var link = args.getLink();
	var start = link.getStartPoint();
	var end = link.getEndPoint();

	// make link horizontal if close to being one
	if (Math.abs(start.x - end.x) > 10 && Math.abs(start.y - end.y) < 4)
	{
		end.y = start.y;
		link.setEndPoint(end);
	}

	// make link vertical if close to being one
	if (Math.abs(start.y - end.y) > 10 && Math.abs(start.x - end.x) < 4)
	{
		end.x = start.x;
		link.setEndPoint(end);
	}
}

Here’s the kind of block diagrams and flowcharts you can now draw:
Block diagram

The complete example can be downloaded from this link:
https://mindfusion.eu/_samples/BlockDiagEdit.zip

Enjoy!

Custom colors in maps

In a series of blog posts, we will explore various usage scenarios for MindFusion software components, based mostly on technical support questions we are frequently asked. Today’s post shows how to assign colors to map regions, depending on the value of a field in the map’s associated DBF database. This particular example assigns darker shades of red to countries with larger populations.

Let’s load an ESRI map file and its database into the map view’s BaseMap property, which provides a shortcut for setting a map in the first layer of the view:

// load the map file and dbf database
var map = mapView.BaseMap = Map.FromFile(
	"ne_50m_admin_0_countries.shp", true, "NAME");
var db = map.Database;
var layer = mapView.Layers[0];

Next, create an array of population threshold values, which will correspond to different values in the layer’s color palette:

// specify threshold values that trigger more saturated colors
var m = 1000000;
var populationThreshold = new[]
{
	0, 1*m, 5*m, 10*m, 50*m, 100*m, 1000*m
};

int numColors = populationThreshold.Length;

Define the map colors as shades of red:

// set the layer palette with a color for each threshold value
layer.FillColors = new Color[numColors];
layer.FillColors[0] = Color.WhiteSmoke;
for (int i = 1; i < numColors; i++)
{
	layer.FillColors[i] = Color.FromArgb(
		255,
		255 - 255 / (numColors - i),
		255 - 255 / (numColors - i));
}

Iterate over the database records, find their corresponding shape from the ESRI .shp file, and read the population field:

// for each database record, read population field and set color
for (int i = 0; i < db.Rows.Count; i++)
{
	var countryShape = map.Shapes[i];
	var population = float.Parse(db.Rows[i]["POP_EST"]);
	…
}

Finally, determine the shape’s color from the maximal threshold value smaller than the country’s population:

	for (int c = numColors - 1; c >= 0; c--)
	{
		if (population > populationThreshold[c])
		{
			countryShape.Color = c;
			break;
		}
	}

The resulting custom-colored map is shown in this screenshot:

Colored map
Labels for countries are displayed dynamically e.g. they are hidden if space is not enough. In the sample pictured above you will see all labels if you zoom in the map.

A VS2008 solution that includes the full source code and map files can be downloaded here:
https://mindfusion.eu/_samples/ColorCoding.zip

Enjoy!