Wednesday, March 12, 2008

How to: Create new chart types with Infragistics Ultrachart

The Infragistics NetAdvantage 7.3 release came out with a new event for the Ultrachart user control. The FillSceneGraph event was provided to help those who want to change the chart’s elements right before firing the Paint event. Using this new event we can create new chart types that are not supported by Infragistics, by default.

Infragistics Ultrachart new chart type

I will show you how to create a chart that combines the following two chart types: Column and StackColumn. This chart type will be able to render for each period (series label) a stack column, with any number of segments, followed by any number of simple columns.

First, let’s prepare the working chart using the Infragistics Ultrachart wizard to create one chart area and two chart layer, one for each chart type we want to use. Those charts can share the same axes.
Infragistics Ultrachart wizard

To achieve our purpose we have to use this namespaces:

C# .NET

using Infragistics.UltraChart.Resources.Appearance;

using Infragistics.UltraChart.Shared.Styles;

using Infragistics.UltraChart.Shared.Events;

using Infragistics.UltraChart.Core.Primitives;


Here are the global members that will help us to handle the chart data:

C# .NET

// Space between two serieses.

private int seriesSpacing = 10;

// Maximum with a column or a stack column can have.

private int maximumWidth;

// Percentage value to specify how wide a column or a

// stack column will be.

// Value 1 means the maximum width will be used.

private float wideFactor = 0.8F;

// How many columns will be rendered in the chart.

// (including the stack columns).

private int columnsNo = 8;

// Precalculated locations for each rendered column reffering the stack column

// rendered into each period.

// The dictionary Key is the chart's period index.

// The Value is a list of X axis offsets for each containing column.

private Dictionary<int, List<int>> columnsLocations = new Dictionary<int,List<int>>();


Now let’s continue by populating the chart with some data and creating a initialize method to be called each time the FillSceneGraph event is fired. This may occur when the chart is resized for example. Actually, each time the Pain event is about to be fired.

C# .NET

private void GetSomeChartData()

{

    // Populate the stack column layer...

    NumericSeries sc1 = new NumericSeries();

    sc1.Points.Add(new NumericDataPoint(4, "1", false));

    sc1.Points.Add(new NumericDataPoint(1, "1", false));

    sc1.Points.Add(new NumericDataPoint(5, "1", false));

    this.ultraChart1.CompositeChart.ChartLayers[0].Series.Add(sc1);

    NumericSeries sc2 = new NumericSeries();

    sc2.Points.Add(new NumericDataPoint(8, "2", false));

    sc2.Points.Add(new NumericDataPoint(3, "2", false));

    sc2.Points.Add(new NumericDataPoint(2, "2", false));

    this.ultraChart1.CompositeChart.ChartLayers[0].Series.Add(sc2);

    // Populate the column layer...

    NumericSeries c1 = new NumericSeries();

    c1.Points.Add(new NumericDataPoint(6, "1", false));

    c1.Points.Add(new NumericDataPoint(5, "1", false));

    c1.Points.Add(new NumericDataPoint(9, "1", false));

    this.ultraChart1.CompositeChart.ChartLayers[1].Series.Add(c1);           

    NumericSeries c2 = new NumericSeries();

    c2.Points.Add(new NumericDataPoint(8, "2", false));

    c2.Points.Add(new NumericDataPoint(3, "2", false));

    c2.Points.Add(new NumericDataPoint(4, "2", false));

    this.ultraChart1.CompositeChart.ChartLayers[1].Series.Add(c2);           

}



private void Init()

{

    this.columnsLocations.Clear();

    // Determin the maximum value a column or a stack column can have...

    this.maximumWidth = Convert.ToInt32((this.ultraChart1.CompositeChart.ChartAreas[0].InnerBounds.Width - this.seriesSpacing) / this.columnsNo);

}



The next step is to identify the chart data we need to handle. For our chart type that should be the elements used to render the columns and the stack columns. For both we will handle Box primitives. A Box primitive is an Ultrachart class used to render a rectangular element into the chart. It contains information about the parent chart layer, size, location and color used for filling it up. Other chart types may work with different primitives (lines for example). We will know for each Box what chart type it will be used to render by checking the containing layer.

We will separate the primitives used to render the Column chart and the StackColumn chart into separate lists.

C# .NET

private void IdentifyChartElements(FillSceneGraphEventArgs sceneGraph,

    List<Box> stackColumnBoxes,

    List<Box> columnBoxes)

{

  if (sceneGraph != null &&

    stackColumnBoxes != null &&

    columnBoxes != null)

    {

        foreach (Primitive primitive in sceneGraph.SceneGraph)

        {

            NumericSeries nns = primitive.Series as NumericSeries;

            Box box = primitive as Box;

            if (box != null)

            {

                // Ignore the chart area (which is also a box)

                if (box.Row < 0 || box.Column < 0 ||

                    (box.rect.X == 0 && box.rect.Y == 0))

                {

                    continue;

                }

                // Get the chart layer for the current rendered primitive to

                // find out if it will be renderd as a Column or as a StackColumn...

                ChartLayerAppearance layer = this.ultraChart1.CompositeChart.ChartLayers.FromKey (box.Layer.LayerID);

                if (layer != null)

                {

                    if (layer.ChartType == ChartType.ColumnChart)

                    {

                        columnBoxes.Add(box);

                    }

                    else if (layer.ChartType == ChartType.StackColumnChart)

                    {

                        stackColumnBoxes.Add(box);

                    }

                }

            }

        }

    }

}


We create a method for rendering each of our base chart types:

C# .NET

private void RenderStackColumns(List<Box> stackColumnBoxes)

{

    if (stackColumnBoxes != null)

    {

        foreach (Box stackColumnBox in stackColumnBoxes)

        {

            // On each period, the stack column is rendered first so

            // its location will consider the series spacing value...

            stackColumnBox.rect.X += this.seriesSpacing;

            // This is the X axis center value of the rendered

            // stack column.

            float stackColumnCenter = stackColumnBox.rect.X + this.maximumWidth / 2;

            if (!this.columnsLocations.ContainsKey(stackColumnBox.Row))

            {

                this.columnsLocations.Add(stackColumnBox.Row, new List<int>());

                for (int i = 1; i < this.columnsNo; i++)

                {

                    this.columnsLocations[stackColumnBox.Row].Add(Convert.ToInt32(stackColumnCenter + i * this.maximumWidth));

                }

            }

            // Compute the new stack column width value and location...

            stackColumnBox.rect.Width = Convert.ToInt32(this.maximumWidth * this.wideFactor);

            stackColumnBox.rect.X = Convert.ToInt32(stackColumnCenter - stackColumnBox.rect.Width / 2);

        }

    }

}

C# .NET

private void RenderColumns(List<Box> columnBoxes)

{

    if (columnBoxes != null)

    {

        // It is used to spot the moment

        int previousRowIndex = -1;

        // Specifies which location will be used for each column.

        int columnLocationIndex = 0;

        foreach (Box columnBox in columnBoxes)

        {

            if (columnBox.Row > previousRowIndex)

            {

                columnLocationIndex = 0;

                previousRowIndex = columnBox.Row;

            }

            // This is the X axis center value of the rendered column.

            float columnCenter = this.columnsLocations[columnBox.Row][columnLocationIndex];

            // Compute the new column width value and location...

            columnBox.rect.Width = Convert.ToInt32(this.maximumWidth * this.wideFactor);

            columnBox.rect.X = Convert.ToInt32(columnCenter - columnBox.rect.Width / 2);

            columnLocationIndex++;

        }

    }

}



Finally this is how everything is used with the FillSceneGraph event:

C# .NET

private void ultraChart1_FillSceneGraph(object sender, FillSceneGraphEventArgs e)

{

    // A list with all the primitives that will be

    // rendered as the ColumnStack.

    List<Box> columnStackBoxes = new List<Box>();

    // A list with all the primitives that will be

    // rendered as the Columns.

    List<Box> columnBoxes = new List<Box>();                     

    // Preparing the chart rendering...

    this.Init();

    // Identify which chart element will be redered as column and which as

    // stack column...

    this.IdentifyChartElements(e, columnStackBoxes, columnBoxes);

    // First render the stack columns...

    this.RenderStackColumns(columnStackBoxes);

    // Then, render the simple columns...

    this.RenderColumns(columnBoxes);

}



kick it on DotNetKicks.com

1 comment:

NeilL said...

I'm trying to use the FillSceneGraph event to modify the Legend for my chart.

Your blog entry is helpful in trying to figure out how this event works. Do you know of or have any examples of either adding a custom Legend or modifying an existing legend. My objective is to calculate the required Legend height (SpanPercentage) to make sure that all legend items are displayed.