使用 OxyPlot 的新绘图类型
New Plot Type using OxyPlot
我正在尝试在 OxyPlot 中创建新的绘图类型。我本质上需要一个 StairStepSeries,但是任何负值都会被它们的 Math.Abs
值替换,当这种情况发生时,反映这种情况的线条样式已经发生(通过使用颜色和/或 LineStyle
)。所以,突出我想要的东西
为此,我创建了两个 classes(我粘贴了下面使用的实际代码)。当您知道您正在使用的工具时,这在概念上很容易,而我不知道。我的问题与我对rectangle.DrawClippedLineSegments()
的不当使用直接相关。我可以获得标准的 StairStepSeries
绘图(复制内部代码),但是当我尝试直观地使用 rectangle.DrawClippedLineSegments()
时,我意识到我不知道该方法的作用或应该如何使用,但找不到任何文档。 rectangle.DrawClippedLineSegments()
在做什么以及应该如何使用此方法?
感谢您的宝贵时间。
代码:
namespace OxyPlot.Series
{
using System;
using System.Collections.Generic;
using OxyPlot.Series;
/// <summary>
/// Are we reversing positive of negative values?
/// </summary>
public enum ThresholdType { ReflectAbove, ReflectBelow };
/// <summary>
/// Class that renders absolute positive and absolute negative values
/// but changes the line style according to those values that changed sign.
/// The value at which the absolute vaue is taken can be manually set.
/// </summary>
public class AbsoluteStairStepSeries : StairStepSeries
{
/// <summary>
/// The default color used when a value is reversed accross the threshold.
/// </summary>
private OxyColor defaultColorThreshold;
#region Initialization.
/// <summary>
/// Default ctor.
/// </summary>
public AbsoluteStairStepSeries()
{
this.Threshold = 0.0;
this.ThresholdType = OxyPlot.Series.ThresholdType.ReflectAbove;
this.ColorThreshold = this.ActualColor;
this.LineStyleThreshold = OxyPlot.LineStyle.LongDash;
}
#endregion // Initialization.
/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="model">The model.</param>
protected override void SetDefaultValues(PlotModel model)
{
base.SetDefaultValues(model);
if (this.ColorThreshold.IsAutomatic())
this.defaultColorThreshold = model.GetDefaultColor();
if (this.LineStyleThreshold == LineStyle.Automatic)
this.LineStyleThreshold = model.GetDefaultLineStyle();
}
/// <summary>
/// Renders the LineSeries on the specified rendering context.
/// </summary>
/// <param name="rc">The rendering context.</param>
/// <param name="model">The owner plot model.</param>
public override void Render(IRenderContext rc, PlotModel model)
{
if (this.ActualPoints.Count == 0)
return;
// Set defaults.
this.VerifyAxes();
OxyRect clippingRect = this.GetClippingRect();
double[] dashArray = this.ActualDashArray;
double[] verticalLineDashArray = this.VerticalLineStyle.GetDashArray();
LineStyle lineStyle = this.ActualLineStyle;
double verticalStrokeThickness = double.IsNaN(this.VerticalStrokeThickness) ?
this.StrokeThickness : this.VerticalStrokeThickness;
OxyColor actualColor = this.GetSelectableColor(this.ActualColor);
// Perform thresholding on clipping rectangle.
//double threshold = this.YAxis.Transform(this.Threshold);
//switch (ThresholdType)
//{
// // reflect any values below the threshold above the threshold.
// case ThresholdType.ReflectAbove:
// //if (clippingRect.Bottom < threshold)
// clippingRect.Bottom = threshold;
// break;
// case ThresholdType.ReflectBelow:
// break;
// default:
// break;
//}
// Perform the render action.
Action<IList<ScreenPoint>, IList<ScreenPoint>> renderPoints = (lpts, mpts) =>
{
// Clip the line segments with the clipping rectangle.
if (this.StrokeThickness > 0 && lineStyle != LineStyle.None)
{
if (!verticalStrokeThickness.Equals(this.StrokeThickness) ||
this.VerticalLineStyle != lineStyle)
{
// TODO: change to array
List<ScreenPoint> hlptsOk = new List<ScreenPoint>();
List<ScreenPoint> vlptsOk = new List<ScreenPoint>();
List<ScreenPoint> hlptsFlip = new List<ScreenPoint>();
List<ScreenPoint> vlptsFlip = new List<ScreenPoint>();
double threshold = this.YAxis.Transform(this.Threshold);
for (int i = 0; i + 2 < lpts.Count; i += 2)
{
switch (ThresholdType)
{
case ThresholdType.ReflectAbove:
clippingRect.Bottom = threshold;
if (lpts[i].Y < threshold)
hlptsFlip.Add(new ScreenPoint(lpts[i].X, threshold - lpts[i].Y));
else
hlptsOk.Add(lpts[i]);
if (lpts[i + 1].Y < threshold)
{
ScreenPoint tmp = new ScreenPoint(
lpts[i + 1].X, threshold - lpts[i + 1].Y);
hlptsFlip.Add(tmp);
vlptsFlip.Add(tmp);
}
else
{
hlptsOk.Add(lpts[i + 1]);
vlptsOk.Add(lpts[i + 1]);
}
if (lpts[i + 2].Y < threshold)
vlptsFlip.Add(new ScreenPoint(lpts[i + 2].X, threshold - lpts[i + 2].Y));
else
vlptsOk.Add(lpts[i + 2]);
break;
case ThresholdType.ReflectBelow:
break;
default:
break;
}
}
//for (int i = 0; i + 2 < lpts.Count; i += 2)
//{
// hlpts.Add(lpts[i]);
// hlpts.Add(lpts[i + 1]);
// vlpts.Add(lpts[i + 1]);
// vlpts.Add(lpts[i + 2]);
//}
rc.DrawClippedLineSegments(
clippingRect,
hlptsOk,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
hlptsFlip,
OxyColor.FromRgb(255, 0, 0),
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsOk,
actualColor,
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsFlip,
OxyColor.FromRgb(255, 0, 0),
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
}
else
{
rc.DrawClippedLine(
clippingRect,
lpts,
0,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
}
}
if (this.MarkerType != MarkerType.None)
{
rc.DrawMarkers(
clippingRect,
mpts,
this.MarkerType,
this.MarkerOutline,
new[] { this.MarkerSize },
this.MarkerFill,
this.MarkerStroke,
this.MarkerStrokeThickness);
}
};
// Transform all points to screen coordinates
// Render the line when invalid points occur.
var linePoints = new List<ScreenPoint>();
var markerPoints = new List<ScreenPoint>();
double previousY = double.NaN;
foreach (var point in this.ActualPoints)
{
if (!this.IsValidPoint(point))
{
renderPoints(linePoints, markerPoints);
linePoints.Clear();
markerPoints.Clear();
previousY = double.NaN;
continue;
}
var transformedPoint = this.Transform(point);
if (!double.IsNaN(previousY))
{
// Horizontal line from the previous point to the current x-coordinate
linePoints.Add(new ScreenPoint(transformedPoint.X, previousY));
}
linePoints.Add(transformedPoint);
markerPoints.Add(transformedPoint);
previousY = transformedPoint.Y;
}
renderPoints(linePoints, markerPoints);
if (this.LabelFormatString != null)
{
// Render point labels (not optimized for performance).
this.RenderPointLabels(rc, clippingRect);
}
}
#region Properties.
/// <summary>
/// The value, positive or negative at which any values are reversed
/// accross the threshold.
/// </summary>
public double Threshold { get; set; }
/// <summary>
/// Hold the thresholding type.
/// </summary>
public ThresholdType ThresholdType { get; set; }
/// <summary>
/// Gets or sets the color for the part of the
/// line that is above/below the threshold.
/// </summary>
public OxyColor ColorThreshold { get; set; }
/// <summary>
/// Gets the actual threshold color.
/// </summary>
/// <value>The actual color.</value>
public OxyColor ActualColorThreshold
{
get { return this.ColorThreshold.GetActualColor(this.defaultColorThreshold); }
}
/// <summary>
/// Gets or sets the line style for the part of the
/// line that is above/below the threshold.
/// </summary>
/// <value>The line style.</value>
public LineStyle LineStyleThreshold { get; set; }
/// <summary>
/// Gets the actual line style for the part of the
/// line that is above/below the threshold.
/// </summary>
/// <value>The line style.</value>
public LineStyle ActualLineStyleThreshold
{
get
{
return this.LineStyleThreshold != LineStyle.Automatic ?
this.LineStyleThreshold : LineStyle.Solid;
}
}
#endregion // Properties.
}
}
和 WPF class
namespace OxyPlot.Wpf
{
using System.Windows;
using System.Windows.Media;
using OxyPlot.Series;
/// <summary>
/// The WPF wrapper for OxyPlot.AbsoluteStairStepSeries.
/// </summary>
public class AbsoluteStairStepSeries : StairStepSeries
{
/// <summary>
/// Default ctor.
/// </summary>
public AbsoluteStairStepSeries()
{
this.InternalSeries = new OxyPlot.Series.AbsoluteStairStepSeries();
}
/// <summary>
/// Creates the internal series.
/// </summary>
/// <returns>
/// The internal series.
/// </returns>
public override OxyPlot.Series.Series CreateModel()
{
this.SynchronizeProperties(this.InternalSeries);
return this.InternalSeries;
}
/// <summary>
/// Synchronizes the properties.
/// </summary>
/// <param name="series">The series.</param>
protected override void SynchronizeProperties(OxyPlot.Series.Series series)
{
base.SynchronizeProperties(series);
var s = series as OxyPlot.Series.AbsoluteStairStepSeries;
s.Threshold = this.Threshold;
s.ColorThreshold = this.ColorThreshold.ToOxyColor();
}
/// <summary>
/// Identifies the <see cref="Threshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register(
"Threshold", typeof(double), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(0.0, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="ThresholdType"/> dependency property.
/// </summary>
public static readonly DependencyProperty ThresholdTypeProperty = DependencyProperty.Register(
"ThresholdType", typeof(ThresholdType), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(ThresholdType.ReflectAbove, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="ColorThreshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty ColorThresholdProperty = DependencyProperty.Register(
"ColorThreshold", typeof(Color), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(Colors.Red, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="LineStyleThreshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty LineStyleThresholdProperty = DependencyProperty.Register(
"LineStyleThreshold", typeof(LineStyle), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(LineStyle.LongDash, AppearanceChanged));
/// <summary>
/// Get or set the threshold value.
/// </summary>
public double Threshold
{
get { return (double)GetValue(ThresholdProperty); }
set { SetValue(ThresholdProperty, value); }
}
/// <summary>
/// Get or set the threshold type to be used.
/// </summary>
public ThresholdType ThresholdType
{
get { return (ThresholdType)GetValue(ThresholdTypeProperty); }
set { SetValue(ThresholdTypeProperty, value); }
}
/// <summary>
/// Get or set the threshold color.
/// </summary>
public Color ColorThreshold
{
get { return (Color)GetValue(ColorThresholdProperty); }
set { SetValue(ColorThresholdProperty, value); }
}
/// <summary>
/// Get or set the threshold line style.
/// </summary>
public LineStyle LineStyleThreshold
{
get { return (LineStyle)GetValue(LineStyleThresholdProperty); }
set { SetValue(LineStyleThresholdProperty, value); }
}
}
}
我有机会对此进行了研究,虽然我建议的可能不是理想的解决方案,但它应该会给你一些有用的帮助。
首先,DrawClippedLineSegments
(可以查看源码here)及其对应的扩展方法(DrawClippedRectangleAsPolygon
、DrawClippedEllipse
等)用于绘制各种将图形绘制到主要 plot/rendering 区域。提供给此方法的裁剪矩形代表可以绘制图形的区域,我们不希望在该区域之外绘制任何东西,因为它不在轴限制内,看起来很奇怪,也不会有特别的好处。在您的例子中,您向它传递了一个数据点列表,以及它们的计算渲染位置;只有裁剪矩形内的数据点才会绘制在您的绘图上。
您可以在该源文件的第 118 行看到裁剪计算的开始 var clipping = new CohenSutherlandClipping(clippingRectangle);
- 这不是我特别熟悉的内容,但快速 wikipedia 搜索表明它是一种专门用于计算线剪裁的算法,在该源文件中其他地方使用的其他算法中最少。我认为您不需要更改裁剪矩形,除非其中一个数据点的反转会将其放置在当前绘制区域之外。
关于实际帮助找到解决方案,在探索您的代码时我注意到了几件事。我尝试的第一件事是绘制一些数据点(全部为正值),发现整个图都是倒置的,主要是因为以下语句:
if (lpts[i].Y < threshold)
对于正值始终为真。这是 Y 轴坐标系从 window 的顶部开始并向 window 的底部增加的结果。由于我的阈值是 0
,当转换到屏幕上的渲染位置时,每个正数据点的 Y
位置将小于轴 Y
值;本质上,您关于哪些点被翻转或不翻转的逻辑需要反转。这应该让你得到你想要的行为(确保翻转点计算正确。)
替代方法
我没有深入到裁剪矩形/计算转换数据点的方法,而是选择了一种稍微懒惰的方法,它可以从一些整理中受益,但根据您的要求可能会有用。
我决定在调用实际渲染点之前执行阈值 flipping/amendment。
我以最小的方式通过这些更改(渲染方法)更改了您的 AbsoluteStairStepSeries
class,保留了大部分现有结构:
public override void Render(IRenderContext rc, PlotModel model)
{
if (this.ActualPoints.Count == 0)
return;
// Set defaults.
this.VerifyAxes();
OxyRect clippingRect = this.GetClippingRect();
double[] dashArray = this.ActualDashArray;
double[] verticalLineDashArray = this.VerticalLineStyle.GetDashArray();
LineStyle lineStyle = this.ActualLineStyle;
double verticalStrokeThickness = double.IsNaN(this.VerticalStrokeThickness) ?
this.StrokeThickness : this.VerticalStrokeThickness;
OxyColor actualColor = this.GetSelectableColor(this.ActualColor);
// Perform the render action.
Action<IList<Tuple<bool, ScreenPoint>>, IList<Tuple<bool, ScreenPoint>>> renderPoints = (lpts, mpts) =>
{
// Clip the line segments with the clipping rectangle.
if (this.StrokeThickness > 0 && lineStyle != LineStyle.None)
{
if (!verticalStrokeThickness.Equals(this.StrokeThickness) ||
this.VerticalLineStyle != lineStyle)
{
// TODO: change to array
List<ScreenPoint> hlptsOk = new List<ScreenPoint>();
List<ScreenPoint> vlptsOk = new List<ScreenPoint>();
List<ScreenPoint> hlptsFlip = new List<ScreenPoint>();
List<ScreenPoint> vlptsFlip = new List<ScreenPoint>();
double threshold = this.YAxis.Transform(this.Threshold);
for (int i = 0; i + 2 < lpts.Count; i += 2)
{
hlptsOk.Add(lpts[i].Item2);
hlptsOk.Add(lpts[i + 1].Item2);
vlptsOk.Add(lpts[i + 1].Item2);
vlptsOk.Add(lpts[i + 2].Item2);
// Add flipped points so they may be overdrawn.
if (lpts[i].Item1 == true)
{
hlptsFlip.Add(lpts[i].Item2);
hlptsFlip.Add(lpts[i + 1].Item2);
}
}
rc.DrawClippedLineSegments(
clippingRect,
hlptsOk,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
hlptsFlip,
OxyColor.FromRgb(255, 0, 0),
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsOk,
actualColor,
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsFlip,
OxyColor.FromRgb(255, 0, 0),
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
}
else
{
rc.DrawClippedLine(
clippingRect,
lpts.Select(x => x.Item2).ToList(),
0,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
}
}
if (this.MarkerType != MarkerType.None)
{
rc.DrawMarkers(
clippingRect,
mpts.Select(x => x.Item2).ToList(),
this.MarkerType,
this.MarkerOutline,
new[] { this.MarkerSize },
this.MarkerFill,
this.MarkerStroke,
this.MarkerStrokeThickness);
}
};
// Transform all points to screen coordinates
// Render the line when invalid points occur.
var linePoints = new List<Tuple<bool, ScreenPoint>>();
var markerPoints = new List<Tuple<bool, ScreenPoint>>();
double previousY = double.NaN;
foreach (var point in this.ActualPoints)
{
var localPoint = point;
bool pointAltered = false;
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
if (!this.IsValidPoint(localPoint))
{
renderPoints(linePoints, markerPoints);
linePoints.Clear();
markerPoints.Clear();
previousY = double.NaN;
continue;
}
var transformedPoint = this.Transform(localPoint);
if (!double.IsNaN(previousY))
{
// Horizontal line from the previous point to the current x-coordinate
linePoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, new ScreenPoint(transformedPoint.X, previousY)));
}
linePoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, transformedPoint));
markerPoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, transformedPoint));
previousY = transformedPoint.Y;
}
renderPoints(linePoints, markerPoints);
if (this.LabelFormatString != null)
{
// Render point labels (not optimized for performance).
this.RenderPointLabels(rc, clippingRect);
}
}
我使用 List<Tuple<bool, ScreenPoint>>
而不是 List<ScreenPoint>
来存储每个点的布尔标志,表示该点是否已被更改;你可以使用一个小的 class 来简化语法。
因为您直接与点数据交互,所以您无需担心屏幕位置(反向 Y 轴),因此从概念上讲,取绝对值的计算更容易阅读:
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
我注意到您的代码反映了下面的 above/reflect,这可能是您在需要时插入此处的逻辑,我已经选择 Math.Abs
,您提到的是您的初始要求。
在实际渲染线条时,我保留了绘制 StepSeries
的原始代码,因此实际上整个系列都以绿色绘制。我只添加了一个条件语句来检查 modified/reflected 点,如果找到,相关的绘图点将添加到包含翻转点的现有列表中,然后以红色绘制。
Tuples
使渲染方法有点乱(添加 Item1/Item2 ),您可以删除修改点的双重绘制,但我认为结果是什么你在追求(或者肯定能为你指明正确的方向。
示例行为:
我正在尝试在 OxyPlot 中创建新的绘图类型。我本质上需要一个 StairStepSeries,但是任何负值都会被它们的 Math.Abs
值替换,当这种情况发生时,反映这种情况的线条样式已经发生(通过使用颜色和/或 LineStyle
)。所以,突出我想要的东西
为此,我创建了两个 classes(我粘贴了下面使用的实际代码)。当您知道您正在使用的工具时,这在概念上很容易,而我不知道。我的问题与我对rectangle.DrawClippedLineSegments()
的不当使用直接相关。我可以获得标准的 StairStepSeries
绘图(复制内部代码),但是当我尝试直观地使用 rectangle.DrawClippedLineSegments()
时,我意识到我不知道该方法的作用或应该如何使用,但找不到任何文档。 rectangle.DrawClippedLineSegments()
在做什么以及应该如何使用此方法?
感谢您的宝贵时间。
代码:
namespace OxyPlot.Series
{
using System;
using System.Collections.Generic;
using OxyPlot.Series;
/// <summary>
/// Are we reversing positive of negative values?
/// </summary>
public enum ThresholdType { ReflectAbove, ReflectBelow };
/// <summary>
/// Class that renders absolute positive and absolute negative values
/// but changes the line style according to those values that changed sign.
/// The value at which the absolute vaue is taken can be manually set.
/// </summary>
public class AbsoluteStairStepSeries : StairStepSeries
{
/// <summary>
/// The default color used when a value is reversed accross the threshold.
/// </summary>
private OxyColor defaultColorThreshold;
#region Initialization.
/// <summary>
/// Default ctor.
/// </summary>
public AbsoluteStairStepSeries()
{
this.Threshold = 0.0;
this.ThresholdType = OxyPlot.Series.ThresholdType.ReflectAbove;
this.ColorThreshold = this.ActualColor;
this.LineStyleThreshold = OxyPlot.LineStyle.LongDash;
}
#endregion // Initialization.
/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="model">The model.</param>
protected override void SetDefaultValues(PlotModel model)
{
base.SetDefaultValues(model);
if (this.ColorThreshold.IsAutomatic())
this.defaultColorThreshold = model.GetDefaultColor();
if (this.LineStyleThreshold == LineStyle.Automatic)
this.LineStyleThreshold = model.GetDefaultLineStyle();
}
/// <summary>
/// Renders the LineSeries on the specified rendering context.
/// </summary>
/// <param name="rc">The rendering context.</param>
/// <param name="model">The owner plot model.</param>
public override void Render(IRenderContext rc, PlotModel model)
{
if (this.ActualPoints.Count == 0)
return;
// Set defaults.
this.VerifyAxes();
OxyRect clippingRect = this.GetClippingRect();
double[] dashArray = this.ActualDashArray;
double[] verticalLineDashArray = this.VerticalLineStyle.GetDashArray();
LineStyle lineStyle = this.ActualLineStyle;
double verticalStrokeThickness = double.IsNaN(this.VerticalStrokeThickness) ?
this.StrokeThickness : this.VerticalStrokeThickness;
OxyColor actualColor = this.GetSelectableColor(this.ActualColor);
// Perform thresholding on clipping rectangle.
//double threshold = this.YAxis.Transform(this.Threshold);
//switch (ThresholdType)
//{
// // reflect any values below the threshold above the threshold.
// case ThresholdType.ReflectAbove:
// //if (clippingRect.Bottom < threshold)
// clippingRect.Bottom = threshold;
// break;
// case ThresholdType.ReflectBelow:
// break;
// default:
// break;
//}
// Perform the render action.
Action<IList<ScreenPoint>, IList<ScreenPoint>> renderPoints = (lpts, mpts) =>
{
// Clip the line segments with the clipping rectangle.
if (this.StrokeThickness > 0 && lineStyle != LineStyle.None)
{
if (!verticalStrokeThickness.Equals(this.StrokeThickness) ||
this.VerticalLineStyle != lineStyle)
{
// TODO: change to array
List<ScreenPoint> hlptsOk = new List<ScreenPoint>();
List<ScreenPoint> vlptsOk = new List<ScreenPoint>();
List<ScreenPoint> hlptsFlip = new List<ScreenPoint>();
List<ScreenPoint> vlptsFlip = new List<ScreenPoint>();
double threshold = this.YAxis.Transform(this.Threshold);
for (int i = 0; i + 2 < lpts.Count; i += 2)
{
switch (ThresholdType)
{
case ThresholdType.ReflectAbove:
clippingRect.Bottom = threshold;
if (lpts[i].Y < threshold)
hlptsFlip.Add(new ScreenPoint(lpts[i].X, threshold - lpts[i].Y));
else
hlptsOk.Add(lpts[i]);
if (lpts[i + 1].Y < threshold)
{
ScreenPoint tmp = new ScreenPoint(
lpts[i + 1].X, threshold - lpts[i + 1].Y);
hlptsFlip.Add(tmp);
vlptsFlip.Add(tmp);
}
else
{
hlptsOk.Add(lpts[i + 1]);
vlptsOk.Add(lpts[i + 1]);
}
if (lpts[i + 2].Y < threshold)
vlptsFlip.Add(new ScreenPoint(lpts[i + 2].X, threshold - lpts[i + 2].Y));
else
vlptsOk.Add(lpts[i + 2]);
break;
case ThresholdType.ReflectBelow:
break;
default:
break;
}
}
//for (int i = 0; i + 2 < lpts.Count; i += 2)
//{
// hlpts.Add(lpts[i]);
// hlpts.Add(lpts[i + 1]);
// vlpts.Add(lpts[i + 1]);
// vlpts.Add(lpts[i + 2]);
//}
rc.DrawClippedLineSegments(
clippingRect,
hlptsOk,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
hlptsFlip,
OxyColor.FromRgb(255, 0, 0),
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsOk,
actualColor,
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsFlip,
OxyColor.FromRgb(255, 0, 0),
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
}
else
{
rc.DrawClippedLine(
clippingRect,
lpts,
0,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
}
}
if (this.MarkerType != MarkerType.None)
{
rc.DrawMarkers(
clippingRect,
mpts,
this.MarkerType,
this.MarkerOutline,
new[] { this.MarkerSize },
this.MarkerFill,
this.MarkerStroke,
this.MarkerStrokeThickness);
}
};
// Transform all points to screen coordinates
// Render the line when invalid points occur.
var linePoints = new List<ScreenPoint>();
var markerPoints = new List<ScreenPoint>();
double previousY = double.NaN;
foreach (var point in this.ActualPoints)
{
if (!this.IsValidPoint(point))
{
renderPoints(linePoints, markerPoints);
linePoints.Clear();
markerPoints.Clear();
previousY = double.NaN;
continue;
}
var transformedPoint = this.Transform(point);
if (!double.IsNaN(previousY))
{
// Horizontal line from the previous point to the current x-coordinate
linePoints.Add(new ScreenPoint(transformedPoint.X, previousY));
}
linePoints.Add(transformedPoint);
markerPoints.Add(transformedPoint);
previousY = transformedPoint.Y;
}
renderPoints(linePoints, markerPoints);
if (this.LabelFormatString != null)
{
// Render point labels (not optimized for performance).
this.RenderPointLabels(rc, clippingRect);
}
}
#region Properties.
/// <summary>
/// The value, positive or negative at which any values are reversed
/// accross the threshold.
/// </summary>
public double Threshold { get; set; }
/// <summary>
/// Hold the thresholding type.
/// </summary>
public ThresholdType ThresholdType { get; set; }
/// <summary>
/// Gets or sets the color for the part of the
/// line that is above/below the threshold.
/// </summary>
public OxyColor ColorThreshold { get; set; }
/// <summary>
/// Gets the actual threshold color.
/// </summary>
/// <value>The actual color.</value>
public OxyColor ActualColorThreshold
{
get { return this.ColorThreshold.GetActualColor(this.defaultColorThreshold); }
}
/// <summary>
/// Gets or sets the line style for the part of the
/// line that is above/below the threshold.
/// </summary>
/// <value>The line style.</value>
public LineStyle LineStyleThreshold { get; set; }
/// <summary>
/// Gets the actual line style for the part of the
/// line that is above/below the threshold.
/// </summary>
/// <value>The line style.</value>
public LineStyle ActualLineStyleThreshold
{
get
{
return this.LineStyleThreshold != LineStyle.Automatic ?
this.LineStyleThreshold : LineStyle.Solid;
}
}
#endregion // Properties.
}
}
和 WPF class
namespace OxyPlot.Wpf
{
using System.Windows;
using System.Windows.Media;
using OxyPlot.Series;
/// <summary>
/// The WPF wrapper for OxyPlot.AbsoluteStairStepSeries.
/// </summary>
public class AbsoluteStairStepSeries : StairStepSeries
{
/// <summary>
/// Default ctor.
/// </summary>
public AbsoluteStairStepSeries()
{
this.InternalSeries = new OxyPlot.Series.AbsoluteStairStepSeries();
}
/// <summary>
/// Creates the internal series.
/// </summary>
/// <returns>
/// The internal series.
/// </returns>
public override OxyPlot.Series.Series CreateModel()
{
this.SynchronizeProperties(this.InternalSeries);
return this.InternalSeries;
}
/// <summary>
/// Synchronizes the properties.
/// </summary>
/// <param name="series">The series.</param>
protected override void SynchronizeProperties(OxyPlot.Series.Series series)
{
base.SynchronizeProperties(series);
var s = series as OxyPlot.Series.AbsoluteStairStepSeries;
s.Threshold = this.Threshold;
s.ColorThreshold = this.ColorThreshold.ToOxyColor();
}
/// <summary>
/// Identifies the <see cref="Threshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register(
"Threshold", typeof(double), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(0.0, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="ThresholdType"/> dependency property.
/// </summary>
public static readonly DependencyProperty ThresholdTypeProperty = DependencyProperty.Register(
"ThresholdType", typeof(ThresholdType), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(ThresholdType.ReflectAbove, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="ColorThreshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty ColorThresholdProperty = DependencyProperty.Register(
"ColorThreshold", typeof(Color), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(Colors.Red, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="LineStyleThreshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty LineStyleThresholdProperty = DependencyProperty.Register(
"LineStyleThreshold", typeof(LineStyle), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(LineStyle.LongDash, AppearanceChanged));
/// <summary>
/// Get or set the threshold value.
/// </summary>
public double Threshold
{
get { return (double)GetValue(ThresholdProperty); }
set { SetValue(ThresholdProperty, value); }
}
/// <summary>
/// Get or set the threshold type to be used.
/// </summary>
public ThresholdType ThresholdType
{
get { return (ThresholdType)GetValue(ThresholdTypeProperty); }
set { SetValue(ThresholdTypeProperty, value); }
}
/// <summary>
/// Get or set the threshold color.
/// </summary>
public Color ColorThreshold
{
get { return (Color)GetValue(ColorThresholdProperty); }
set { SetValue(ColorThresholdProperty, value); }
}
/// <summary>
/// Get or set the threshold line style.
/// </summary>
public LineStyle LineStyleThreshold
{
get { return (LineStyle)GetValue(LineStyleThresholdProperty); }
set { SetValue(LineStyleThresholdProperty, value); }
}
}
}
我有机会对此进行了研究,虽然我建议的可能不是理想的解决方案,但它应该会给你一些有用的帮助。
首先,DrawClippedLineSegments
(可以查看源码here)及其对应的扩展方法(DrawClippedRectangleAsPolygon
、DrawClippedEllipse
等)用于绘制各种将图形绘制到主要 plot/rendering 区域。提供给此方法的裁剪矩形代表可以绘制图形的区域,我们不希望在该区域之外绘制任何东西,因为它不在轴限制内,看起来很奇怪,也不会有特别的好处。在您的例子中,您向它传递了一个数据点列表,以及它们的计算渲染位置;只有裁剪矩形内的数据点才会绘制在您的绘图上。
您可以在该源文件的第 118 行看到裁剪计算的开始 var clipping = new CohenSutherlandClipping(clippingRectangle);
- 这不是我特别熟悉的内容,但快速 wikipedia 搜索表明它是一种专门用于计算线剪裁的算法,在该源文件中其他地方使用的其他算法中最少。我认为您不需要更改裁剪矩形,除非其中一个数据点的反转会将其放置在当前绘制区域之外。
关于实际帮助找到解决方案,在探索您的代码时我注意到了几件事。我尝试的第一件事是绘制一些数据点(全部为正值),发现整个图都是倒置的,主要是因为以下语句:
if (lpts[i].Y < threshold)
对于正值始终为真。这是 Y 轴坐标系从 window 的顶部开始并向 window 的底部增加的结果。由于我的阈值是 0
,当转换到屏幕上的渲染位置时,每个正数据点的 Y
位置将小于轴 Y
值;本质上,您关于哪些点被翻转或不翻转的逻辑需要反转。这应该让你得到你想要的行为(确保翻转点计算正确。)
替代方法
我没有深入到裁剪矩形/计算转换数据点的方法,而是选择了一种稍微懒惰的方法,它可以从一些整理中受益,但根据您的要求可能会有用。
我决定在调用实际渲染点之前执行阈值 flipping/amendment。
我以最小的方式通过这些更改(渲染方法)更改了您的 AbsoluteStairStepSeries
class,保留了大部分现有结构:
public override void Render(IRenderContext rc, PlotModel model)
{
if (this.ActualPoints.Count == 0)
return;
// Set defaults.
this.VerifyAxes();
OxyRect clippingRect = this.GetClippingRect();
double[] dashArray = this.ActualDashArray;
double[] verticalLineDashArray = this.VerticalLineStyle.GetDashArray();
LineStyle lineStyle = this.ActualLineStyle;
double verticalStrokeThickness = double.IsNaN(this.VerticalStrokeThickness) ?
this.StrokeThickness : this.VerticalStrokeThickness;
OxyColor actualColor = this.GetSelectableColor(this.ActualColor);
// Perform the render action.
Action<IList<Tuple<bool, ScreenPoint>>, IList<Tuple<bool, ScreenPoint>>> renderPoints = (lpts, mpts) =>
{
// Clip the line segments with the clipping rectangle.
if (this.StrokeThickness > 0 && lineStyle != LineStyle.None)
{
if (!verticalStrokeThickness.Equals(this.StrokeThickness) ||
this.VerticalLineStyle != lineStyle)
{
// TODO: change to array
List<ScreenPoint> hlptsOk = new List<ScreenPoint>();
List<ScreenPoint> vlptsOk = new List<ScreenPoint>();
List<ScreenPoint> hlptsFlip = new List<ScreenPoint>();
List<ScreenPoint> vlptsFlip = new List<ScreenPoint>();
double threshold = this.YAxis.Transform(this.Threshold);
for (int i = 0; i + 2 < lpts.Count; i += 2)
{
hlptsOk.Add(lpts[i].Item2);
hlptsOk.Add(lpts[i + 1].Item2);
vlptsOk.Add(lpts[i + 1].Item2);
vlptsOk.Add(lpts[i + 2].Item2);
// Add flipped points so they may be overdrawn.
if (lpts[i].Item1 == true)
{
hlptsFlip.Add(lpts[i].Item2);
hlptsFlip.Add(lpts[i + 1].Item2);
}
}
rc.DrawClippedLineSegments(
clippingRect,
hlptsOk,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
hlptsFlip,
OxyColor.FromRgb(255, 0, 0),
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsOk,
actualColor,
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsFlip,
OxyColor.FromRgb(255, 0, 0),
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
}
else
{
rc.DrawClippedLine(
clippingRect,
lpts.Select(x => x.Item2).ToList(),
0,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
}
}
if (this.MarkerType != MarkerType.None)
{
rc.DrawMarkers(
clippingRect,
mpts.Select(x => x.Item2).ToList(),
this.MarkerType,
this.MarkerOutline,
new[] { this.MarkerSize },
this.MarkerFill,
this.MarkerStroke,
this.MarkerStrokeThickness);
}
};
// Transform all points to screen coordinates
// Render the line when invalid points occur.
var linePoints = new List<Tuple<bool, ScreenPoint>>();
var markerPoints = new List<Tuple<bool, ScreenPoint>>();
double previousY = double.NaN;
foreach (var point in this.ActualPoints)
{
var localPoint = point;
bool pointAltered = false;
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
if (!this.IsValidPoint(localPoint))
{
renderPoints(linePoints, markerPoints);
linePoints.Clear();
markerPoints.Clear();
previousY = double.NaN;
continue;
}
var transformedPoint = this.Transform(localPoint);
if (!double.IsNaN(previousY))
{
// Horizontal line from the previous point to the current x-coordinate
linePoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, new ScreenPoint(transformedPoint.X, previousY)));
}
linePoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, transformedPoint));
markerPoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, transformedPoint));
previousY = transformedPoint.Y;
}
renderPoints(linePoints, markerPoints);
if (this.LabelFormatString != null)
{
// Render point labels (not optimized for performance).
this.RenderPointLabels(rc, clippingRect);
}
}
我使用 List<Tuple<bool, ScreenPoint>>
而不是 List<ScreenPoint>
来存储每个点的布尔标志,表示该点是否已被更改;你可以使用一个小的 class 来简化语法。
因为您直接与点数据交互,所以您无需担心屏幕位置(反向 Y 轴),因此从概念上讲,取绝对值的计算更容易阅读:
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
我注意到您的代码反映了下面的 above/reflect,这可能是您在需要时插入此处的逻辑,我已经选择 Math.Abs
,您提到的是您的初始要求。
在实际渲染线条时,我保留了绘制 StepSeries
的原始代码,因此实际上整个系列都以绿色绘制。我只添加了一个条件语句来检查 modified/reflected 点,如果找到,相关的绘图点将添加到包含翻转点的现有列表中,然后以红色绘制。
Tuples
使渲染方法有点乱(添加 Item1/Item2 ),您可以删除修改点的双重绘制,但我认为结果是什么你在追求(或者肯定能为你指明正确的方向。
示例行为: