This blog post is part of a series on plotting with OxyPlot (see Part 1, Part 2, Part 3, and Part 4).
So far, you’ve used the LineSeries object to plot lines for each structure’s DVH. OxyPlot also supports other kinds of plots, including scatter, column, pie, area, heat map, and contour. Refer to the OxyPlot documentation for the full list of supported plot types. I also recommend downloading the OxyPlot source code and running the sample application. It showcases many of the plot types and features of OxyPlot.
In this blog post, you’re going to create three different plots: column, pie, and heat map. Because you’re already familiar with the basic code that gets the plot shown in a window, you won’t see it again here. Instead, you’ll only see the relevant code for plotting a specific kind of plot. You can easily modify the scripts from previous sections to show these new plots.
Column
The column plot shows you the data as vertical columns. In this example, you’re going to plot the meterset units (MU) for each of the beams in a plan. This data is readily available from ESAPI.
private PlotModel CreatePlotModel()
{
var plotModel = new PlotModel();
AddAxes(plotModel);
AddSeries(plotModel);
return plotModel;
}
private void AddAxes(PlotModel plotModel)
{
var xAxis = new CategoryAxis
{Title = "Beam", Position = AxisPosition.Bottom};
var beams = _plan.Beams.Where(b => !b.IsSetupField).Take(5);
var beamIds = beams.Select(b => b.Id);
xAxis.Labels.AddRange(beamIds);
plotModel.Axes.Add(xAxis);
var yAxis = new LinearAxis
{Title = "Meterset [MU]", Position = AxisPosition.Left};
plotModel.Axes.Add(yAxis);
}
private void AddSeries(PlotModel plotModel)
{
var series = new ColumnSeries();
var beams = _plan.Beams.Where(b => !b.IsSetupField).Take(5);
var items = beams.Select(b => new ColumnItem(b.Meterset.Value));
series.Items.AddRange(items);
plotModel.Series.Add(series);
}
The column plot requires a category axis, which is specified by adding a CategoryAxis object to the plot’s axes. The label of each category should be the ID of the beam. To do that, you first obtain the beams that are not "setup" beams, and then extract their IDs. These IDs are then added to the Labels property of the CategoryAxis.
To create the series, you first create a ColumnSeries object. Again, you are only interested in the non-setup beams from the plan. For each beam, you create a ColumnItem, which you initialize with the beam’s MU value. Finally, these items are added to the Items property of the series.
The resulting plot looks like this:
This is the default look. Remember that you can change almost anything about a plot. For column plots, you can change things like their color, outline, and the space between columns. There is also a BarSeries object that shows the columns horizontally rather than vertically.
Pie
If you’re interested in the contribution of each beam to the total MU, you can show the data above as a pie. The pie plot doesn’t need any axes, so remove the call to AddAxes in the CreatePlotModel method. Then, update the AddSeries method to the following:
private void AddSeries(PlotModel plotModel)
{
var series = new PieSeries
{
InsideLabelColor = OxyColors.White,
OutsideLabelFormat = "{0:f0} MU ({2:f0}%)"
};
var beams = _plan.Beams.Where(b => !b.IsSetupField).Take(5);
var slices = beams.Select(b => new PieSlice(b.Id, b.Meterset.Value));
foreach (var slice in slices)
series.Slices.Add(slice);
plotModel.Series.Add(series);
}
First, you create a PieSeries object and specify that the labels should be white (the default black labels don’t contrast well against the colors of the pie slices). I’ll discuss the OutsideLabelFormat property later. As before, you obtain the non-setup beams. Next, you create a PieSlice object for each beam, passing it a label (in this case, the beam ID) and a value (in this case, the MU). You then add each slice to the series and finally add the series to the PlotModel. Here’s what the plot looks like now:
The format of the outside labels is specified by the OutsideLabelFormat property of the PieSeries object. The format is specified as a composite format string, which uses placeholders that are replaced when displayed to the user. The OutsideLabelFormat recognizes three placeholders: {0}, which represents the numerical value, {1}, which represents the textual label, and {2}, which represents the percentage. For beam A-0, for example, {0} would be replaced with 173, {1} would be replaced with "A-0," and {2} would be replaced with 25%. When you don’t specify an OutsideLabelFormat, the label only shows the percentage.
The placeholders representing numerical values can themselves be formatted to show or hide their decimal parts. For example, if you want to show the MU with one decimal number, you’d use "{0:f1}". For no decimal numbers, you’d use "{0:f0}". Therefore, the format used in the code, "{0:f0} MU ({2:f0}%)", says to include the MU value, the unit (MU), and in parenthesis the percentage, ending in a % symbol.
Heat map
The heat map shows you the data in a matrix as a color wash, similar to Eclipse’s dose color wash. In fact, the following example will show you how to plot the dose distribution of a plan as a color wash. In addition, you’ll be able to choose the slice (or plane) you want to see using a slider control.
Starting with the DvhPlot script you’ve worked on previously, modify the MainView XAML to add a slider:
<DockPanel Grid.Column="1">
<Slider
DockPanel.Dock="Top"
Value="{Binding PlaneIndex}"
Minimum="0"
Maximum="{Binding MaximumPlaneIndex}"
Orientation="Horizontal"
/>
<oxy:PlotView
DockPanel.Dock="Top"
Model="{Binding PlotModel}"
/>
</DockPanel>
The DockPanel wraps the Slider and the PlotView. The DockPanel is another kind of panel, like the Grid, but allows its items to be docked or stacked in a specific way. An interesting property of the DockPanel is that, by default, the last element is resized to fill any remaining space. Because the PlotView is the last element, it will be resized automatically as the window is resized.
The Slider will let the user choose the plane index of the dose matrix. The Value of the slider is data-bound to the PlaneIndex property of the view model. This means that as the user moves the slider, the PlaneIndex will be updated automatically. The minimum and maximum values of the slider will represent the lowest and highest plane indexes. Because the maximum plane index isn’t known until the script is running, the Maximum property is bound to MaximumPlaneIndex, which is determined from the dose matrix at runtime.
The heat map requires a LinearColorAxis, which describes how the colors will be shown. Open the MainViewModel class, and update the AddAxes method as follows:
private void AddAxes(PlotModel plotModel)
{
var xAxis = new LinearAxis
{Title = "X", Position = AxisPosition.Bottom,};
plotModel.Axes.Add(xAxis);
var yAxis = new LinearAxis
{Title = "Y", Position = AxisPosition.Left};
plotModel.Axes.Add(yAxis);
var zAxis = new LinearColorAxis
{
Title = "Dose [Gy]",
Position = AxisPosition.Top,
Palette = OxyPalettes.Rainbow(256),
Maximum = 33.1
};
plotModel.Axes.Add(zAxis);
}
The x- and y-axes are the same as before, except for the titles. The z-axis, which represents the range of dose values as a color scale, will be shown above the plot (AxisPosition.Top). The color scale can use any number of colors (or palette) to represent the range of values. In this case, the Palette property is set to Rainbow, where violet and blue represent low values, and red represent high values (with all other colors in between). The number passed to Rainbow is the number of color levels to use. Finally, the maximum value of the axis scale is specified. Normally, this value is determined automatically from the data, But because the data will change as the slider is moved, the maximum value needs to be fixed for the entire dose matrix.
Now, modify the AddSeries method to add the heat map:
private void AddSeries(PlotModel plotModel)
{
plotModel.Series.Add(CreateHeatMap());
}
private Series CreateHeatMap()
{
return new HeatMapSeries
{
X0 = 0, X1 = _plan.Dose.XSize - 1,
Y0 = 0, Y1 = _plan.Dose.YSize - 1,
Data = GetDoseData()
};
}
private double[,] GetDoseData()
{
_plan.DoseValuePresentation = DoseValuePresentation.Absolute;
var dose = _plan.Dose;
var data = new int[dose.XSize, dose.YSize];
dose.GetVoxels((int)PlaneIndex, data);
return ConvertToDoseMatrix(data);
}
private double[,] ConvertToDoseMatrix(int[,] ints)
{
var dose = _plan.Dose;
var doseMatrix = new double[dose.XSize, dose.YSize];
for (int i = 0; i < dose.XSize; i++)
for (int j = 0; j < dose.YSize; j++)
doseMatrix[i, j] = dose.VoxelToDoseValue(ints[i, j]).Dose;
return doseMatrix;
}
The CreateHeatMap method instantiates a new HeatMapSeries object. The x and y limits of the axes need to be specified. These correspond to the dose matrix’s limits, which can be obtained using ESAPI. For simplicity, the upper limits are set to the last voxel in the corresponding axis, but you can modify this to physical units (such as cm).
The Data property expects a two-dimensional array of type double, which is created from the dose matrix in the GetDoseData method. First, the DoseValuePresentation is set to Absolute to ensure the doses are in absolute units. Then, the GetVoxels method of the Dose object is called to obtain the dose matrix at the current plane index. The current plane index is stored in the PlaneIndex property (more on that later). Finally, the raw voxel values are converted to dose values using the ESAPI method VoxelToDoseValue.
The final piece of code is to add define the properties the slider is bound to:
private double _planeIndex;
public double PlaneIndex
{
get { return _planeIndex; }
set
{
_planeIndex = value;
PlotModel.Series[0] = CreateHeatMap();
PlotModel.InvalidatePlot(false);
}
}
public int MaximumPlaneIndex
{
get { return _plan.Dose.ZSize - 1; }
}
The PlaneIndex property represents the index of the current plane the plot is showing. When it’s changed using the slider, the property is changed via its set method. When this happens, the entire heat map is re-created because a new dose plane needs to be calculated. The plot model is then invalidated in order to redraw the plot.
The MaximumPlaneIndex simply returns the last plane index of the dose. This lets the slider set the correct range of allowed values the user can change to. It is data-bound to the Maximum property of the slider.
If you run the script, the final plot should look like the following:
Notice the slider control above the plot. As you drag it, the heat map is updated with the correct dose plane.