堆积条形图 Silverlight 控件
Silverlight 的堆积条形图控件。
Silverlight 工具包为开发者和设计师提供了大量标准中不包含的不同控件。 其中包括用于创建条形图、饼图和折线图等的图表控件。 我最近有一个需求是需要一个堆叠条形图,不幸的是,这是少数几种默认情况下不受支持的图表类型之一。 然而,在阅读了 Jafar Husain 发表的一系列文章并查看了工具包源代码后,我意识到我可以相对容易地自己创建一个堆叠条形图——尽管我对 Silverlight 的了解有限。 在这里,我必须向 Jafar 道歉——我通过他的博客向他寻求建议,他回复了一个非常全面的描述,说明了他将如何解决这个问题。 Jafar 的方法会产生一个更完整、更灵活的解决方案; 然而,对于我需要的东西来说,这有些过头了,而且我必须尽快完成一些工作,所以我采取了一种(可以说)更hacky的方法。
我采用的方法是创建一个新的 StackedColumnSeries
,它是 System.Windows.Controls.DataVisualization.Charting.ColumnSeries
类的完整副本。 在这一点上,能够使用 ColumnSeries
作为 StackedColumnSeries
的基类将会很棒,但由于 ColumnSeries
是 sealed
的,我不得不复制和粘贴。 为了使我的 ColumnSeries
副本能够编译,我还需要从工具包中复制和粘贴其他一些代码块,因为各种辅助类等是内部的。 如果您 下载代码,您会看到我清楚地注释了哪些代码是我复制的,因此,如果工具包团队将来稍微开放他们的代码,那么更改 StackedColumnSeries
类将会非常简单。
一旦 StackedColumnSeries
类编译并通过了与 ColumnSeries
相同的功能,我只需要更改两个方法才能使其像堆叠柱状图一样工作。 我更改的方法是 UpdateDataPoint(DataPoint dataPoint)
,以便可以渲染列,使其彼此堆叠,以及 GetValueMargins(IValueMarginConsumer consumer)
,以便我可以增加轴上显示的值的范围,考虑到现在需要显示的最大值是堆叠列的总和。
下面显示了这两个更改的方法——请参阅代码中的注释,了解我需要对从 ColumnSeries
复制的代码进行的更改。
/// <summary>
/// Updates each point.
/// </summary>
/// <param name="dataPoint">The data point to update.</param>
protected override void UpdateDataPoint(DataPoint dataPoint)
{
if (SeriesHost == null || PlotArea == null)
{
return;
}
object category = dataPoint.IndependentValue ??
(this.ActiveDataPoints.IndexOf(dataPoint) + 1);
Range<UnitValue> coordinateRange =
GetCategoryRange(category);
if (!coordinateRange.HasData)
{
return;
}
else if (coordinateRange.Maximum.Unit != Unit.Pixels ||
coordinateRange.Minimum.Unit != Unit.Pixels)
{
throw new InvalidOperationException("This Series " +
"Does Not Support Radial Axes");
}
double minimum = (double)coordinateRange.Minimum.Value;
double maximum = (double)coordinateRange.Maximum.Value;
double plotAreaHeight = ActualDependentRangeAxis.GetPlotAreaCoordinate(
ActualDependentRangeAxis.Range.Maximum).Value.Value;
IEnumerable<StackedColumnSeries> columnSeries =
SeriesHost.Series.OfType<StackedColumnSeries>().Where(series =>
series.ActualIndependentAxis == ActualIndependentAxis);
int numberOfSeries = columnSeries.Count();
double coordinateRangeWidth = (maximum - minimum);
double segmentWidth = coordinateRangeWidth * 0.8;
// For stacked chart, no need to scale
// the width of the columns if there are more series
double columnWidth = segmentWidth; // / numberOfSeries;
int seriesIndex = columnSeries.IndexOf(this);
double dataPointY = ActualDependentRangeAxis.GetPlotAreaCoordinate(
ValueHelper.ToDouble(dataPoint.ActualDependentValue)).Value.Value;
double zeroPointY = ActualDependentRangeAxis.GetPlotAreaCoordinate(
ActualDependentRangeAxis.Origin).Value.Value;
// Need to shift the columns up to take account of the other
// columns that are already rendered, to make them
// appear stacked
int dataPointIndex = ActiveDataPoints.IndexOf(dataPoint);
for (int i = numberOfSeries - 1; i > seriesIndex; i--)
{
StackedColumnSeries prevSeries = columnSeries.ElementAt<StackedColumnSeries>(i);
if (prevSeries.ActiveDataPointCount >= dataPointIndex + 1)
{
double yOffset = ActualDependentRangeAxis.GetPlotAreaCoordinate(
ValueHelper.ToDouble(prevSeries.ActiveDataPoints.ElementAt<DataPoint>(
dataPointIndex).ActualDependentValue)).Value.Value;
dataPointY += yOffset;
zeroPointY += yOffset;
}
}
// No offset for stacked bar charts so that all the columns line up
double offset = 0; /*seriesIndex * Math.Round(columnWidth) +
coordinateRangeWidth * 0.1;*/
double dataPointX = minimum + offset;
if (GetIsDataPointGrouped(category))
{
// Multiple DataPoints share this category;
// offset and overlap them appropriately
IGrouping<object, DataPoint> categoryGrouping =
GetDataPointGroup(category);
int index = categoryGrouping.IndexOf(dataPoint);
dataPointX += (index * (columnWidth * 0.2)) /
(categoryGrouping.Count() - 1);
columnWidth *= 0.8;
Canvas.SetZIndex(dataPoint, -index);
}
if (ValueHelper.CanGraph(dataPointY) &&
ValueHelper.CanGraph(dataPointX) &&
ValueHelper.CanGraph(zeroPointY))
{
// Remember, the coordinate 0,0 is in the top left hand corner,
// therefore the "top" y coordinate is going to
// be a smaller value than the bottom.
double left = Math.Round(dataPointX);
double width = Math.Round(columnWidth);
double top = Math.Round(plotAreaHeight -
Math.Max(dataPointY, zeroPointY) + 0.5);
double bottom = Math.Round(plotAreaHeight -
Math.Min(dataPointY, zeroPointY) + 0.5);
double height = bottom - top + 1;
Canvas.SetLeft(dataPoint, left);
Canvas.SetTop(dataPoint, top);
dataPoint.Width = width;
dataPoint.Height = height;
}
}
/// <summary>
/// Returns the value margins for a given axis.
/// </summary>
/// <param name="consumer">The axis to retrieve the value margins for.
/// </param>
/// <returns>A sequence of value margins.</returns>
protected override IEnumerable<ValueMargin>
GetValueMargins(IValueMarginConsumer consumer)
{
double dependentValueMargin = this.ActualHeight / 10;
IAxis axis = consumer as IAxis;
if (axis != null && ActiveDataPoints.Any())
{
Func<DataPoint, IComparable> selector = null;
if (axis == InternalActualIndependentAxis)
{
selector = (dataPoint) => (IComparable)dataPoint.ActualIndependentValue;
DataPoint minimumPoint = ActiveDataPoints.MinOrNull(selector);
DataPoint maximumPoint = ActiveDataPoints.MaxOrNull(selector);
double minimumMargin = minimumPoint.GetMargin(axis);
yield return new ValueMargin(selector(minimumPoint),
minimumMargin, minimumMargin);
double maximumMargin = maximumPoint.GetMargin(axis);
yield return new ValueMargin(selector(maximumPoint),
maximumMargin, maximumMargin);
}
else if (axis == InternalActualDependentAxis)
{
// Some up the total value of the current set of columns
double valueTotal = 0.0;
foreach (DataPoint dataPoint in
ActiveDataPoints.AsEnumerable<DataPoint>())
{
valueTotal += (double)dataPoint.ActualDependentValue;
}
// The minimum value always needs to be 0 for stacked column charts
yield return new ValueMargin(0.0, dependentValueMargin,
dependentValueMargin);
yield return new ValueMargin(valueTotal, dependentValueMargin,
dependentValueMargin);
}
}
else
{
yield break;
}
}
运行此代码并将一些数据绑定到图表的结果如下所示(请注意,我添加了一条系列线)