Charts

Overview

The Curl® language Charts package is built on the Shapes package. It enables you to display data in charts and graphs. You need to put the data into a RecordSet before you use it in a Chart. You can also use ConnectedRecordSet to chart data from a database server. See Data Access for more information on using RecordSet and ConnectedRecordSet. Charts created with the Curl charts package update the chart when data in the associated record set changes.
The Curl Charts API attempts to use reasonable colors and create appropriate axes and legends by default. The API also provides means for you to customize these chart components.
Chart is the base class for charts. The charts API provides the subclasses LayeredChart and PieChart. LayeredChart is the chart type for charts that plot data in two dimensions using X and Y coordinates. Different types of charts, such as bar charts and line charts, are implemented as ChartLayer objects. You can add one or more ChartLayer objects to a LayeredChart to create more complex charts. The API provides these chart layers:
A PieChart contains one or more PieSets.

Layered Charts

The following sections describe the types of chart layers.

Line Layer

LineLayer draws lines connecting the data points in the data series. The following example plots the braking distance of a typical automobile as a function of vehicle speed. The example plots the following data series:
The line chart clearly shows the increase in braking distance with increased vehicle speed.
The example also supplies a ChartAxis with an axis-label for the left axis. The default label for the axis is a list of the names or captions of the RecordSet fields that supply data to the chart. In this case, the list of captions is too long to fit in the 6cm height of the chart, which can cause the chart to draw incorrectly. The supplied axis label fits and is more informative. The left ChartAxis is based on a ChartDataSeries constructed from the field in the source record set that contains the overall distance values. Because the overall distance is the sum of the thinking and braking distances, this field contains the largest distance values, making it the best choice to determine the axis range. See LayeredChart Axes for more information on working with chart axes.
The ChartAxis supplied for the bottom axis sets the keyword force-zero? to false, which allows the axis to begin at a numeric value that fits the plotted data. See Controlling the Range of an Axis. This axis also illustrates the nonlocal option tick-label-auto-stagger?. Its default value is true, which places tick labels at two different levels as they are here.

Example: Line chart
{import * from CURL.GUI.CHARTS}
{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField "KPH", caption = "Kilometers/hour", domain = int},
            {RecordField "TD", caption = "Thinking distance", domain = int},
            {RecordField "BD", caption = "Braking distance", domain = int},
            {RecordField "OD", caption = "Overall distance", domain = int}
        },
        {RecordData KPH = 32, TD = 6, BD = 6, OD = 12},
        {RecordData KPH = 48, TD = 9, BD = 14, OD = 23},
        {RecordData KPH = 64, TD = 12, BD = 24, OD = 36},
        {RecordData KPH = 80, TD = 15, BD = 38, OD = 53},
        {RecordData KPH = 97, TD = 18, BD = 55, OD = 73},
        {RecordData KPH = 113, TD = 21, BD = 75, OD = 96}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 15cm,
        height = 6cm,
        left-axis = 
            {ChartAxis
                {ChartDataSeries records, "OD"},
                axis-label = "Distance (meters)"
            },
        bottom-axis = 
            {ChartAxis
                {ChartDataSeries records, "KPH"},
                force-zero? = false
            },
        {LineLayer
            {ChartDataSeries records, "OD"},
            {ChartDataSeries records, "BD"},
            {ChartDataSeries records, "TD"}
        }
    }
}
{VBox
    {RecordGrid 
        height = 4cm,
        width = 13cm,
        record-source = records
    },
    chart
}

Area Layer

AreaLayer draws the area under the line defined by the data values. The following example uses the same data set and data series as the example for LineLayer. Drawing the area under the line emphasizes how the the shape of the curve for Thinking distance differs from the others.

Example: Area chart
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField "KPH", caption = "Kilometers/hour", domain = int},
            {RecordField "TD", caption = "Thinking distance", domain = int},
            {RecordField "BD", caption = "Braking distance", domain = int},
            {RecordField "OD", caption = "Overall distance", domain = int}
        },
        {RecordData KPH = 32, TD = 6, BD = 6, OD = 12},
        {RecordData KPH = 48, TD = 9, BD = 14, OD = 23},
        {RecordData KPH = 64, TD = 12, BD = 24, OD = 36},
        {RecordData KPH = 80, TD = 15, BD = 38, OD = 53},
        {RecordData KPH = 97, TD = 18, BD = 55, OD = 73},
        {RecordData KPH = 113, TD = 21, BD = 75, OD = 96}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 15cm,
        height = 6cm,
        left-axis = 
            {ChartAxis
                {ChartDataSeries records, "OD"},
                axis-label = "Distance (meters)"
            },
        bottom-axis = 
            {ChartAxis
                {ChartDataSeries records, "KPH"},
                force-zero? = false
            },
        {AreaLayer
            {ChartDataSeries records, "OD"},
            {ChartDataSeries records, "BD"},
            {ChartDataSeries records, "TD"}
        }
    }
}
{VBox
    {RecordGrid 
        height = 4cm,
        width = 13cm,
        record-source = records
    },
    chart
}

Scatter Layer

ScatterLayer renders the data as points in two-dimensional space. A different ScatterShape, as well as a different color, distinguishes each data series.
The following example uses a scatter chart to plot the minimum, maximum and mean daily temperature in Boston MA for the year 2006. The scatter plot presents this large volume of data in a way that makes visual analysis easier.

Example: Scatter chart
{import * from CURL.GUI.CHARTS}
{let boston-temp:CsvRecordSet =
    {CsvRecordSet
        {url "../../default/support/boston-temp.csv"},
        fields = 
            {RecordFields
                {RecordField "EST", domain = {StandardDateDomain}},
                {RecordField "MaxT-F", caption = "Max Temperature (F)", domain = int},
                {RecordField "MeanT-F", caption = "Mean Temperature (F)", domain = int},
                {RecordField "MinT-F", caption = "Min Temperature (F)", domain = int}
            }
    }
}
{let chart:LayeredChart =    
    {LayeredChart 
        width = 20cm,
        height = 15cm,
        bottom-axis-parent =
            {ShapeGroup
                tick-label-rotation = 0deg,
                tick-label-factory =
                    {proc {axis:ChartAxis, tick:ChartTick, tick-rotation:Angle}:any
                        {return 
                            {format "%d/%d", tick.value.info.month, tick.value.info.day}
                        }
                    }
            },
        bottom-axis = 
            {GenericDataSeriesAxis
                {ChartDataSeries boston-temp, "EST"},
                axis-label = "2006"
            },
        left-axis = 
            {ChartAxis
                {ChartDataSeries boston-temp, "MaxT-F"},
                {ChartDataSeries boston-temp, "MinT-F"},
                axis-label = "Temperature (F)"
            },
        {ScatterLayer
            {ChartDataSeries boston-temp, "MaxT-F"},
            {ChartDataSeries boston-temp, "MeanT-F"},
            {ChartDataSeries boston-temp, "MinT-F"}
        }
    }
}
{VBox
    halign = "center",
    {bold Daily Maximum, Mean, and Minimum Temperature, Boston MA},
    chart
}

Bubble Layer

A BubbleLayer enables you to display additional information with each point on the chart. The position of each point with respect to the x and y axes plots two data values, as the points on a scatter chart do. The height and width of the shape drawn at each point can plot an additional two data values. The primary-size-data determines the horizontal dimension of each bubble, and the secondary-size-data determines the vertical. You can provide only primary-size-data, in which case the values determine the overall size of each bubble.
The following example plots monthly household electrical use for the year 2006, with months on the x axis and Kilowatt hours used on the y axis. The primary-size-data is the average heating degree days for the month, and the secondary-size-data is the average cooling degree days for the month. Heating and cooling degree days are temperature-based indices that reflect energy demand for heating and cooling.
Try removing the secondary-size-data and executing the example. Note that the data bubbles become circles of varying diameter.

Example: Bubble Chart
{import * from CURL.GUI.CHARTS}

{include "../../default/support/electric-use-data.scurl"}

{VBox
    {RecordGrid
        height = 5cm,
        record-source = records
    },
    {LayeredChart
        width = 15cm,
        height = 7.25cm,
        {BubbleLayer
            records,
            "kWh-06",
            scatter-shape = "ellipse",
            x-axis-data =
                {ChartDataSeries records, "Month"},
            primary-size-data =
                {ChartDataSeries records, "CDD-06"},
            secondary-size-data =
                {ChartDataSeries records, "HDD-06"}
        }
    }
}

Bar Layer

BarLayer renders the associated data as a bar chart. This example charts data from the same record set used in the bubble chart example. This chart plots monthly electric usage for the years 2005 and 2006. The chart uses a EnumeratedBarChartAxis for the x axis. This axis is specialized for bar charts. It places a tick at the end of the axis, and positions data points and tick labels between the ticks.

Example: Bar chart
{import * from CURL.GUI.CHARTS}
{include "../../default/support/electric-use-data.scurl"}

{let chart:LayeredChart =
    {LayeredChart
        width = 20cm,
        height = 6cm,
        left-axis = 
            {ChartAxis
                {ChartDataSeries records, "kWh-05"},
                axis-label = "Kilowatt hours"
            },
        {BarLayer
            {ChartDataSeries records, "kWh-05"},
            {ChartDataSeries records, "kWh-06"},
            x-axis-data = {ChartDataSeries records, "Month"}
        }
    }
}
{value chart}

Stackable Chart Layers

All of the ChartLayers provided by the API inherit from StackableChartLayer. The option StackableChartLayer.stacking-mode determines how data series in the chart layer are stacked. Possible values are defined in ChartStackingMode. The default value is "none", so the data series are not stacked. The following example illustrates ChartStackingMode.stacked.

Example: Restructured data and chart
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        {BarLayer
            stacking-mode = ChartStackingMode.stacked,
            {ChartDataSeries records, "M1"},
            {ChartDataSeries records, "M2"},
            {ChartDataSeries records, "M3"},
            x-axis-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}

Pie Charts

Single PieSet

The following example illustrates a simple PieChart, which contains one PieSet. It charts a ChartDataSeries for the M1 RecordField. showing January sales figures for each region as a percentage of the total circle. The label-data property of PieSet provides labels for the pie chart sections.

Example: Pie chart
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int}
        },
        {RecordData Region = "North", M1 = 100},
        {RecordData Region = "East", M1 = 110},
        {RecordData Region = "South", M1 = 140},
        {RecordData Region = "West", M1 = 160}
    }
}
{let chart:PieChart =
    {PieChart
        width = 10cm,
        height = 6cm,
        {PieSet
            {ChartDataSeries records, "M1"},
            label-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    {Fill height = 1cm},
    {text Sales figures for January:},
    {Fill height = 1cm},
    chart
}

Multiple PieSets

You can add more than one PieSet to a PieChart, in a process somewhat analogous to adding multiple chart layers to a layered chart. The result is a chart with the first PieSet at the center of the chart, and additional pie sets arranged in concentric rings in the order in which they were added. The property pie-set-inner-margin sets the spacing between rings. The property PieChart.inner-radius sets the radius of a circle at the center of the inner-most PieSet that changes it to a ring.
The following example adds a PieSet for February sales data, and another for March.

Example: Charting more than one PieSet
{import * from CURL.GUI.CHARTS}
{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}

{let chart:PieChart =
    {PieChart
        width = 10cm,
        height = 6cm,
        pie-set-inner-margin = 1mm,
        {PieSet {ChartDataSeries records, "M1"}},
        {PieSet {ChartDataSeries records, "M2"}},
        {PieSet
            {ChartDataSeries records, "M3"},
            label-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    {Fill height = 1cm},
    {spaced-vbox 
        {text Sales figures for:},
        {text January (inner ring), February (middle ring), 
            March (outer ring)}
    },
    {Fill height = 1cm},
    chart
}

Structuring Data for Charting

The charts API creates charts from data in a RecordSet. The data is likely to originate from a database server, where it is probably stored in a format consistent with good database design practice. This format may not be the most appropriate for creating useful charts. This section discusses how you can best structure you data to create effective and useful charts.
The mechanism for bringing data into a chart is the ChartDataSeries. You can create ChartDataSeries explicitly, or implicitly in the constructor for a ChartLayer or PieSet. A ChartDataSeries contains data values from all Records for a single RecordField in a RecordSet. In other words, a ChartDataSeries contains all the data you see in a column when the RecordSet is displayed in a RecordGrid. Each data value in the ChartDataSeries becomes a visualized data point in the resulting chart; a bar in a bar chart, a data point in a scatter chart, and so forth. The set of values you want to plot in the chart, must appear in a RecordField, or column, in the source RecordSet. The data as stored in the source database, does not necessarily have this structure.
For example, first quarter sales data for four geographic regions might be stored in a database structured like this:

Example: Database data structure
{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField
                "Month",
                domain = int
            },
            {RecordField
                "Value",
                domain = int
            }
        },
        {RecordData Region = "North", Month = 1, Value = 100},
        {RecordData Region = "North", Month = 2, Value = 140},
        {RecordData Region = "North", Month = 3, Value = 130},

        {RecordData Region = "East", Month = 1, Value = 110},
        {RecordData Region = "East", Month = 2, Value = 140},
        {RecordData Region = "East", Month = 3, Value = 170},

        {RecordData Region = "South", Month = 1, Value = 140},
        {RecordData Region = "South", Month = 2, Value = 100},
        {RecordData Region = "South", Month = 3, Value = 130},

        {RecordData Region = "West", Month = 1, Value = 160},
        {RecordData Region = "West", Month = 2, Value = 190},
        {RecordData Region = "West", Month = 3, Value = 140}
    }
}
{RecordGrid height = 4cm, width = 10cm, record-source = records}
Given this data structure, you can create data series containing all the regions, all the months, or all the sales values; but none of these series is very useful in a chart. What you probably want is series containing all the values for January, all the values for February, and all the values for March. You can achieve this easily by restructuring the data as illustrated in the next example. Once you have restructured the data, you can easily generate a chart of monthly sales figures versus region.
Notice that both RecordGrid and charts use RecordField.caption for labeling if it is provided.

Example: Restructured data and chart
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        {BarLayer
            {ChartDataSeries records, "M1"},
            {ChartDataSeries records, "M2"},
            {ChartDataSeries records, "M3"},
            x-axis-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}
When you plan a project involving charts, you need to carefully consider the data you are working with, it how that data is structured. You also need to consider the type of data analysis you need the charts to achieve. Then you can determine the structure you need for the record sets that provide data to your charts, and can write code to perform the necessary conversions.

LayeredChart Axes

Types of Axes

ChartAxis is the abstract base class for the classes that create axes on a LayeredChart. These three subclasses of ChartAxis define the basic axis types:
DataSeriesAxis is an abstract subclass of ChartAxis that is the base class for axes based on ChartDataSeries. It also inherits from Observer, so that axes can respond dynamically to changes in the data in underlying ChartDataSeries. These three classes implement the ChartDataSeries versions of the basic axis types.
The following example illustrates the difference between subclasses of DataSeriesAxis and ChartAxis. As it is initially loaded, the chart uses NumericAxis-of for its Y axis. If you change a data value in the source record set, the change does not cause recalculation of the axis. For instance, change the value for "February" in the region "South" from 100 to 300. Note that the axis does not change, and the corresponding bar simply goes off the chart.
Now, comment this line:
left-axis = {new {NumericAxis-of double}, 0, 200},
and remove the comments from these lines:
left-axis = {new {NumericDataSeriesAxis-of double},
                {ChartDataSeries records, "M2"}
            },
Then execute the example and change some of the numeric values in the February column to larger numbers. Note that the axis changes so that the values stay on the chart. Note also that if you change values for a RecordField other than "February", the axis does not change, because the other two ChartDataSeries are not included in NumericDataSeriesAxis-of.data and therefore not in NumericDataSeriesAxis-of.visible-values.

Example: Difference between DataSeriesAxis and ChartAxis
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField "Region", domain = String},
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        left-axis = {new {NumericAxis-of double}, 0, 200},
||--            left-axis = {new {NumericDataSeriesAxis-of double},
||--                            {ChartDataSeries records, "M2"}
||--                        },
        {BarLayer
            {ChartDataSeries records, "M1"},
            {ChartDataSeries records, "M2"},
            {ChartDataSeries records, "M3"},
            x-axis-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}
When you create these axes, you must supply at least one ChartDataSeries, and you can supply many. They must all be of the same Type with the same Domain.

Position of Axes

Charts can draw axes on any of the four sides of a layered chart, using the options LayeredChart.top-axis, LayeredChart.bottom-axis, LayeredChart.right-axis, and LayeredChart.left-axis. The following example plots the same data as the example for bubble chart. Kilowatt hours and cooling and heating degree days are all plotted ad lines. The scale for Kilowatt hours is on the left and the scale for degree days is on the right.

Example: Positioning chart axes
{import * from CURL.GUI.CHARTS}

{include "../../default/support/electric-use-data.scurl"}

{LayeredChart
    width = 15cm,
    height = 7.25cm,
    left-axis = {ChartAxis
                    {ChartDataSeries records, "kWh-06"}
                },
    right-axis = {ChartAxis 
                     {ChartDataSeries records, "CDD-06"},
                     {ChartDataSeries records, "HDD-06"}
                 },
    {LineLayer
        records,
        "kWh-06",
        x-axis-data =
            {ChartDataSeries records, "Month"},
        {ChartDataSeries records, "CDD-06"},
        {ChartDataSeries records, "HDD-06"}
    }
}
The property LayeredChart.flipped? reverses the orientation of the X and Y axes. In a flipped chart, the X axis is vertical, and the y axis is horizontal, as the following example illustrates.

Example: Flipping the axes
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        flipped? = true,
        {BarLayer
            {ChartDataSeries records, "M1"},
            {ChartDataSeries records, "M2"},
            {ChartDataSeries records, "M3"},
            x-axis-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}

Controlling the Range of an Axis

If you chart the example quarterly sales data as a line chart, all the lines at the top of the chart, because the Y axis shows values from zero to 200, but all the data values are above 100. The class NumericDataSeriesAxis-of enables you to adjust the range of values used on a chart axis. By default, the property force-zero? is true, which adjusts the axis values so that the zero value appears on the chart. Setting force-zero? to false, allows the chart to generate an axis that provides the best fit for the charted values, as illustrated in the following example.
Three additional properties enables you to exert more control over range of values on a chart axis. If force-range? is true, then the chart uses forced-min and forced-max as the low and high values of the axis.
You can also use the method set-forced-range to modify a chart axis dynamically. See NumericDataSeriesAxis-of.set-forced-range for an example.
The three lines that are commented out in the following example use these properties to set the axis range from 80 to 200. Remove the comments and execute the example to see how they change the axis.

Example: Adjusting the range of an axis
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        left-axis = {new {NumericDataSeriesAxis-of double},
                        force-zero? = false,
||--                            force-range? = true,
||--                            forced-min = 80,
||--                            forced-max = 200,
                        {ChartDataSeries records, "M1"},
                        {ChartDataSeries records, "M2"},
                        {ChartDataSeries records, "M3"}
                    },
        {LineLayer
            {ChartDataSeries records, "M1"},
            {ChartDataSeries records, "M2"},
            {ChartDataSeries records, "M3"},
            x-axis-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}

Using Color in Charts

Colors used in charts are established by Chart.color-palette, which is an array of FillPattern. The first color in this array is used for the first ChartDataSeries rendered in the Chart, and so on. By default, this array is set to default-chart-color-palette. The charts API also provides pastel-chart-color-palette as an alternative. You can also define your own color palette and use it to set Chart.color-palette.

Example: Using the pastel color palette
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        color-palette = pastel-chart-color-palette,
        left-axis = {new {NumericDataSeriesAxis-of double},
                        force-zero? = false,
                        {ChartDataSeries records, "M1"},
                        {ChartDataSeries records, "M2"},
                        {ChartDataSeries records, "M3"}
                    },
        {BarLayer
            {ChartDataSeries records, "M1"},
            {ChartDataSeries records, "M2"},
            {ChartDataSeries records, "M3"},
            x-axis-data = {ChartDataSeries records, "Region"}
        }
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}
Chart.color-palette controls the sequence of colors used for all data series in the chart. You can also use ChartLayer.color-associations and DataSeriesColorPair to control color used for a specific data series. The following example uses ChartLayer.append-color-association to add an item to ChartLayer.color-associations.

Example: Using color association
{import * from CURL.GUI.CHARTS}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let bl:BarLayer = 
    {BarLayer
        {ChartDataSeries records, "M1"},
        {ChartDataSeries records, "M2"},
        {ChartDataSeries records, "M3"},
        x-axis-data = {ChartDataSeries records, "Region"}

    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 10cm,
        height = 6cm,
        left-axis = {new {NumericDataSeriesAxis-of double},
                        force-zero? = false,
                        {ChartDataSeries records, "M1"},
                        {ChartDataSeries records, "M2"},
                        {ChartDataSeries records, "M3"}
                    },
        bl
    }
}
{bl.append-color-association
    {DataSeriesColorPair
        {ChartDataSeries records, "M1"},
        "silver"
    }
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}
Layered charts use ChartLayer.color-associations to control the color of specific data series. Pie charts use PieSet.color-associations for the same purpose.

Using Chart Factories

The Curl charts API enables you to supply procedures to modify the way components of charts are generated. The API documentation describes these "factories" in detail. The following list summarizes some of the most important.
The following example uses BarLayer.shape-factory to add event handlers to the bars in a chart. The event handlers change the bar color when you move the mouse pointer over the bar.

Example: Using shape-factory to add event handlers
{import * from CURL.GUI.CHARTS}
{import * from CURL.GRAPHICS.IMAGEFILTER}

{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let chart:LayeredChart =
    {LayeredChart
        width = 15cm,
        height = 6cm,
        bar-border-width = 0px,
        bottom-axis =
            {EnumeratedBarChartAxis
                {ChartDataSeries records, "Region"}
            },
        {BarLayer
            records,
            "M1",
            "M2",
            "M3",
            shape-factory =
                {proc {layer:BarLayer,
                       rectangle:GRect,
                       record:Record,
                       record-index:int,
                       series-index:int,
                       border-width:any,
                       border-line-style:LineStyle
                      }:Shape
                    let constant stock-shape:Shape =
                        {BarLayer.default-shape-factory
                            layer,
                            rectangle,
                            record,
                            record-index,
                            series-index,
                            border-width,
                            border-line-style
                        }
                    let start-color:FillPattern = FillPattern.white
                    {stock-shape.add-event-handler
                        {on PointerEnter at s:Shape do
                            set start-color = s.color
                            set s.color = {brightness-adjust
                                              start-color, 50%} 
                        }
                    }
                    {stock-shape.add-event-handler
                        {on PointerLeave at s:Shape do
                            set s.color = start-color
                        }
                    }
                    {return stock-shape}
                }
        }
    }
}
{value chart}

Adding Shapes to a Chart

You can add additional shape objects to a chart to supply additional text, lines and so forth. The following example adds a RectangleShape which acts as a level line, and a TextShape which gives the value represented by the level line.

Example: Adding shapes to a chart
{import * from CURL.GUI.CHARTS}
{let records:RecordSet =
    {RecordSet
        {RecordFields
            {RecordField
                "Region",
                domain = String
            },
            {RecordField "M1", caption = "January", domain = int},
            {RecordField "M2", caption = "February", domain = int},
            {RecordField "M3", caption = "March", domain = int}
        },
        {RecordData Region = "North", M1 = 100, M2 = 140, M3 = 130 },
        {RecordData Region = "East", M1 = 110, M2 = 140, M3 = 170},
        {RecordData Region = "South", M1 = 140, M2 = 100, M3 = 130},
        {RecordData Region = "West", M1 = 160, M2 = 190, M3 = 140}
    }
}
{let level-value:double = 110}
{let chart:LayeredChart =
    {LayeredChart
        width = 15cm,
        height = 7cm,
        {LineLayer
            records,
            "M1",
            "M2",
            "M3",
            x-axis-data = {ChartDataSeries records, "Region"},
            {on ChartLayoutChanged at ll:LineLayer do
                let constant y:Distance =
                    {ll.chart.left-axis.get-position level-value}
                {ll.add
                    {RectangleShape
                        {GRect 0m, {ll.chart.get-x-axis-length}, .5pt - y, .5pt + y},
                        color = "red",
                        {TextShape
                            "" & level-value,
                            valign = "bottom",
                            translation =
                                {Distance2d
                                    0.9 * {ll.chart.get-x-axis-length},
                                    y
                                }
                        }
                    }
                }
            }
        }
    }    
}
{VBox
    {RecordGrid height = 4cm, width = 10cm, record-source = records},
    chart
}