OxyPlot: How to set ticks/decade to 1 on a logarithmic axis - wpf

I have a log-log plot with both axes in the range 0.1 to 1000. I want only 1 major tick per decade. So far I have found no way to control the tick spacing, except to set IntervalLength as in this code.
var logxAxis = new LogarithmicAxis
{
Position = AxisPosition.Bottom,
Title = "Resistivity of Approaching Bed (ohm-m)",
IntervalLength = 100,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.None,
MinorTickSize = 0.0,
Key = "logx"
};
The default IntervalLength is 60, which gave me 2 ticks/decade. Unfortunately, as I increase the window size of my application the number of major ticks increase. So setting IntervalLength is not an ideal solution. I have looked through the OxyPlot source and found nothing. Is there something I am missing, or perhaps I need to derive my own LogarithmicAxis class?
Edit: I decided to derive my own logarithmic axis and replace the function the generated the ticks.
public override void GetTickValues(out IList<double> majorLabelValues, out IList<double> majorTickValues,
out IList<double> minorTickValues)
{
majorLabelValues = new List<double> { 0.1, 1, 10, 100, 1000 };
majorTickValues = new List<double> { 0.1, 1, 10, 100, 1000 };
minorTickValues = new List<double>();
}
This at least lets me get my application out the door.

After some thought, I decided that hard coding the ticks was not the best idea. I decided to create a logarithmic axis where I could lock in the tick placement, but I could also unlock, and let the built in algorithms of OxyPlot work. You will that I reused some OxyPlot methods that are internal, so I just copied the code into my class and made them private. This problem was a lot easier to solve with access to the code. I am stiil open to hearing other solutions.
public class LockableLogarithmicAxis : LogarithmicAxis
{
#region Properties
public bool IsLocked { get; set; }
public double[] MajorTickPositions { get; set; }
public double[] MinorTickPositions { get; set; }
#endregion
#region Constructor
public LockableLogarithmicAxis()
{
IsLocked = true;
}
#endregion
#region Methods
public override void GetTickValues(out IList<double> majorLabelValues, out IList<double> majorTickValues,
out IList<double> minorTickValues)
{
if (!IsLocked)
{
base.GetTickValues(out majorLabelValues, out majorTickValues, out minorTickValues);
return;
}
if (MajorTickPositions != null && MajorTickPositions.Length > 0)
{
majorTickValues = MajorTickPositions.ToList();
}
else
{
majorTickValues = this.DecadeTicks();
}
if (MinorTickPositions != null && MinorTickPositions.Length > 0)
{
minorTickValues = MinorTickPositions.ToList();
}
else
{
minorTickValues = this.SubdividedDecadeTicks();
}
majorLabelValues = majorTickValues;
}
/// <summary>
/// Calculates ticks of the decades in the axis range with a specified step size.
/// </summary>
/// <param name="step">The step size.</param>
/// <returns>A new IList containing the decade ticks.</returns>
private IList<double> DecadeTicks(double step = 1)
{
return this.PowList(this.LogDecadeTicks(step));
}
/// <summary>
/// Calculates logarithmic ticks of the decades in the axis range with a specified step size.
/// </summary>
/// <param name="step">The step size.</param>
/// <returns>A new IList containing the logarithmic decade ticks.</returns>
private IList<double> LogDecadeTicks(double step = 1)
{
var ret = new List<double>();
if (step > 0)
{
var last = double.NaN;
for (var exponent = Math.Ceiling(this.LogActualMinimum); exponent <= this.LogActualMaximum; exponent += step)
{
if (exponent <= last)
{
break;
}
last = exponent;
if (exponent >= this.LogActualMinimum)
{
ret.Add(exponent);
}
}
}
return ret;
}
/// <summary>
/// Raises all elements of a List to the power of <c>this.Base</c>.
/// </summary>
/// <param name="logInput">The input values.</param>
/// <param name="clip">If true, discards all values that are not in the axis range.</param>
/// <returns>A new IList containing the resulting values.</returns>
private IList<double> PowList(IList<double> logInput, bool clip = false)
{
return
logInput.Where(item => !clip || !(item < this.LogActualMinimum))
.TakeWhile(item => !clip || !(item > this.LogActualMaximum))
.Select(item => Math.Pow(this.Base, item))
.ToList();
}
/// <summary>
/// Calculates ticks of all decades in the axis range and their subdivisions.
/// </summary>
/// <param name="clip">If true (default), the lowest and highest decade are clipped to the axis range.</param>
/// <returns>A new IList containing the decade ticks.</returns>
private IList<double> SubdividedDecadeTicks(bool clip = true)
{
var ret = new List<double>();
for (var exponent = (int)Math.Floor(this.LogActualMinimum); ; exponent++)
{
if (exponent > this.LogActualMaximum)
{
break;
}
var currentDecade = Math.Pow(this.Base, exponent);
for (var mantissa = 1; mantissa < this.Base; mantissa++)
{
var currentValue = currentDecade * mantissa;
if (clip && currentValue < this.ActualMinimum)
{
continue;
}
if (clip && currentValue > this.ActualMaximum)
{
break;
}
ret.Add(currentDecade * mantissa);
}
}
return ret;
}
#endregion
}

Related

WPF Draw custom shapes in canvas

I have an array of lines and all the line are connected, forming a custom shape from it. Also i need to fill it with a color.
A line contains something like this:
"StartX":800.0,
"StartY":600.0,
"EndX":0.0,
"EndY":800.0,
"Radius":800.0
I saw that you can build a polygon from points but in my case i need some curved lines cause i have a radius for them.
<Polygon Points="50, 100 200, 100 200, 200 300, 30"
Stroke="Black" StrokeThickness="4"
Fill="Yellow" />
How can i draw something custom in a canvas?
I forgot to mention that i will build this shape programmatically, not from xaml.
Here is an example of lines array and the expected result:
"$values":[
{
"StartX":0.0,
"StartY":0.0,
"EndX":800.0,
"EndY":0.0,
"Radius":0.0
},
{
"StartX":800.0,
"StartY":0.0,
"EndX":800.0,
"EndY":400.0,
"Radius":0.0
},
{
"StartX":800.0,
"StartY":400.0,
"EndX":700.0,
"EndY":400.0,
"Radius":0.0
},
{
"StartX":700.0,
"StartY":400.0,
"EndX":372.727272727273,
"EndY":497.115756874933,
"Radius":600.0
},
{
"StartX":372.727272727273,
"StartY":497.115756874933,
"EndX":100.0,
"EndY":578.045554270711,
"Radius":500.0
},
{
"StartX":100.0,
"StartY":578.045554270711,
"EndX":0.0,
"EndY":578.045554270711,
"Radius":0.0
},
{
"StartX":0.0,
"StartY":578.045554270711,
"EndX":0.0,
"EndY":0.0,
"Radius":0.0
}
]
and the image is:
You should use Path control and adapt it to your needs.
Drawing wpf
Path drawing
Got from CodeProject Code project article:
public class RoundedCornersPolygon : Shape
{
private readonly Path _path;
#region Properties
private PointCollection _points;
/// <summary>
/// Gets or sets a collection that contains the points of the polygon.
/// </summary>
public PointCollection Points
{
get { return _points; }
set
{
_points = value;
RedrawShape();
}
}
private bool _isClosed;
/// <summary>
/// Gets or sets a value that specifies if the polygon will be closed or not.
/// </summary>
public bool IsClosed
{
get
{
return _isClosed;
}
set
{
_isClosed = value;
RedrawShape();
}
}
private bool _useRoundnessPercentage;
/// <summary>
/// Gets or sets a value that specifies if the ArcRoundness property value will be used as a percentage of the connecting segment or not.
/// </summary>
public bool UseRoundnessPercentage
{
get
{
return _useRoundnessPercentage;
}
set
{
_useRoundnessPercentage = value;
RedrawShape();
}
}
private double _arcRoundness;
/// <summary>
/// Gets or sets a value that specifies the arc roundness.
/// </summary>
public double ArcRoundness
{
get
{
return _arcRoundness;
}
set
{
_arcRoundness = value;
RedrawShape();
}
}
public Geometry Data
{
get
{
return _path.Data;
}
}
#endregion
public RoundedCornersPolygon()
{
var geometry = new PathGeometry();
geometry.Figures.Add(new PathFigure());
_path = new Path {Data = geometry};
Points = new PointCollection();
Points.Changed += Points_Changed;
}
private void Points_Changed(object sender, EventArgs e)
{
RedrawShape();
}
#region Implementation of Shape
protected override Geometry DefiningGeometry
{
get
{
return _path.Data;
}
}
#endregion
#region Private Methods
/// <summary>
/// Redraws the entire shape.
/// </summary>
private void RedrawShape()
{
var pathGeometry = _path.Data as PathGeometry;
if (pathGeometry == null) return;
var pathFigure = pathGeometry.Figures[0];
pathFigure.Segments.Clear();
for (int counter = 0; counter < Points.Count; counter++)
{
switch (counter)
{
case 0:
AddPointToPath(Points[counter], null, null);
break;
case 1:
AddPointToPath(Points[counter], Points[counter - 1], null);
break;
default:
AddPointToPath(Points[counter], Points[counter - 1], Points[counter - 2]);
break;
}
}
if (IsClosed)
CloseFigure(pathFigure);
}
/// <summary>
/// Adds a point to the shape
/// </summary>
/// <param name="currentPoint">The current point added</param>
/// <param name="prevPoint">Previous point</param>
/// <param name="prevPrevPoint">The point before the previous point</param>
private void AddPointToPath(Point currentPoint, Point? prevPoint, Point? prevPrevPoint)
{
if (Points.Count == 0)
return;
var pathGeometry = _path.Data as PathGeometry;
if(pathGeometry == null) return;
var pathFigure = pathGeometry.Figures[0];
//the first point of a polygon
if (prevPoint == null)
{
pathFigure.StartPoint = currentPoint;
}
//second point of the polygon, only a line will be drawn
else if (prevPrevPoint == null)
{
var lines = new LineSegment { Point = currentPoint };
pathFigure.Segments.Add(lines);
}
//third point and above
else
{
ConnectLinePoints(pathFigure, prevPrevPoint.Value, prevPoint.Value, currentPoint, ArcRoundness, UseRoundnessPercentage);
}
}
/// <summary>
/// Adds the segments necessary to close the shape
/// </summary>
/// <param name="pathFigure"></param>
private void CloseFigure(PathFigure pathFigure)
{
//No need to visually close the figure if we don't have at least 3 points.
if (Points.Count < 3)
return;
Point backPoint, nextPoint;
if (UseRoundnessPercentage)
{
backPoint = GetPointAtDistancePercent(Points[Points.Count - 1], Points[0], ArcRoundness, false);
nextPoint = GetPointAtDistancePercent(Points[0], Points[1], ArcRoundness, true);
}
else
{
backPoint = GetPointAtDistance(Points[Points.Count - 1], Points[0], ArcRoundness, false);
nextPoint = GetPointAtDistance(Points[0], Points[1], ArcRoundness, true);
}
ConnectLinePoints(pathFigure, Points[Points.Count - 2], Points[Points.Count - 1], backPoint, ArcRoundness, UseRoundnessPercentage);
var line2 = new QuadraticBezierSegment { Point1 = Points[0], Point2 = nextPoint };
pathFigure.Segments.Add(line2);
pathFigure.StartPoint = nextPoint;
}
/// <summary>
/// Method used to connect 2 segments with a common point, defined by 3 points and aplying an arc segment between them
/// </summary>
/// <param name="pathFigure"></param>
/// <param name="p1">First point, of the first segment</param>
/// <param name="p2">Second point, the common point</param>
/// <param name="p3">Third point, the second point of the second segment</param>
/// <param name="roundness">The roundness of the arc</param>
/// <param name="usePercentage">A value that indicates if the roundness of the arc will be used as a percentage or not</param>
private static void ConnectLinePoints(PathFigure pathFigure, Point p1, Point p2, Point p3, double roundness, bool usePercentage)
{
//The point on the first segment where the curve will start.
Point backPoint;
//The point on the second segment where the curve will end.
Point nextPoint;
if (usePercentage)
{
backPoint = GetPointAtDistancePercent(p1, p2, roundness, false);
nextPoint = GetPointAtDistancePercent(p2, p3, roundness, true);
}
else
{
backPoint = GetPointAtDistance(p1, p2, roundness, false);
nextPoint = GetPointAtDistance(p2, p3, roundness, true);
}
int lastSegmentIndex = pathFigure.Segments.Count - 1;
//Set the ending point of the first segment.
((LineSegment)(pathFigure.Segments[lastSegmentIndex])).Point = backPoint;
//Create and add the curve.
var curve = new QuadraticBezierSegment { Point1 = p2, Point2 = nextPoint };
pathFigure.Segments.Add(curve);
//Create and add the new segment.
var line = new LineSegment { Point = p3 };
pathFigure.Segments.Add(line);
}
/// <summary>
/// Gets a point on a segment, defined by two points, at a given distance.
/// </summary>
/// <param name="p1">First point of the segment</param>
/// <param name="p2">Second point of the segment</param>
/// <param name="distancePercent">Distance percent to the point</param>
/// <param name="firstPoint">A value that indicates if the distance is calculated by the first or the second point</param>
/// <returns></returns>
private static Point GetPointAtDistancePercent(Point p1, Point p2, double distancePercent, bool firstPoint)
{
double rap = firstPoint ? distancePercent / 100 : (100 - distancePercent) / 100;
return new Point(p1.X + (rap * (p2.X - p1.X)), p1.Y + (rap * (p2.Y - p1.Y)));
}
/// <summary>
/// Gets a point on a segment, defined by two points, at a given distance.
/// </summary>
/// <param name="p1">First point of the segment</param>
/// <param name="p2">Second point of the segment</param>
/// <param name="distance">Distance to the point</param>
/// <param name="firstPoint">A value that indicates if the distance is calculated by the first or the second point</param>
/// <returns>The point calculated.</returns>
private static Point GetPointAtDistance(Point p1, Point p2, double distance, bool firstPoint)
{
double segmentLength = Math.Sqrt(Math.Pow((p2.X - p1.X), 2) + Math.Pow((p2.Y - p1.Y), 2));
//The distance cannot be greater than half of the length of the segment
if (distance > (segmentLength / 2))
distance = segmentLength / 2;
double rap = firstPoint ? distance / segmentLength : (segmentLength - distance) / segmentLength;
return new Point(p1.X + (rap * (p2.X - p1.X)), p1.Y + (rap * (p2.Y - p1.Y)));
}
#endregion
}
Use this in your View like this:
... xmlns:CustomRoundedCornersPolygon="clr-namespace:CustomRoundedCornersPolygon" />
<CustomRoundedCornersPolygon:RoundedCornersPolygon Points="50, 100 200, 100 200, 200 300, 30"
StrokeThickness="1" ArcRoundness="25" UseRoundnessPercentage="False" Stroke="Black" IsClosed="True">
</CustomRoundedCornersPolygon:RoundedCornersPolygon>
I have just tried it and it works. Give it a try.
I was able to implement it using ArcSegment
For this i need a starting point and a PathSegmentCollection with all the ArcSegments.
An arc segment has the end point of the arc, the sweep direction (clockwise or counterClockwise), isLargeArc (if it represents the large arc or the small arc of the circle it describes) and a size (this is the radius on x and y axis because the arc segment is actually an ellipse arc)
So i have something like this:
foreach (var line in contour)
{
pathSegmentCollection.Add(new ArcSegment
{
Size = new Size(line.Radius, line.Radius),
Point = new Point(line.EndX, line.EndY),
IsLargeArc = false,
SweepDirection = line.LineType == LineType.Clockwise ? SweepDirection.Clockwise : SweepDirection.Counterclockwise
});
}

New Plot Type using OxyPlot

I am attempting to create a new plot type in OxyPlot. I essentally need a StairStepSeries, but with any negative values replaces by their Math.Abs value and when this occurs, the line style to reflect this has happened (by using color and or LineStyle). So, to highlight what I want
To do this, I have created the two classes (I have pasted actual code used below). This is conceptually easy when you know the tools you are working with, which I don't. My problem is directly related to my improper use of rectangle.DrawClippedLineSegments(). I can get a standard StairStepSeries plotting (copying the internal code) but when I attempt to use the rectangle.DrawClippedLineSegments() intuatively I realise I have not got a clue what this method does or how it is supposed to be used, but can't find any documentation. What is rectangle.DrawClippedLineSegments() doing and how should this method be used?
Thanks for your time.
Code:
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.
}
}
and the 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); }
}
}
}
I had a chance to have a look into this, and whilst what I suggest may not be the ideal solution, it should give you some helpful nudges.
Firstly, DrawClippedLineSegments (You can view the source here) and its extension method counterparts (DrawClippedRectangleAsPolygon, DrawClippedEllipse, etc.) are used to draw various plot graphics onto the main plot/rendering area. The clipping rectangle supplied to this method represents the area in which the graph can be plotted, we don't want anything drawn outside of this area, as it wouldn't be within the axis limits, would look odd, and wouldn't be of particular benefit. In your case, you're passing it a list of data points, along with their calculated rendering locations; only data points within the clipping rectangle will be drawn on your plot.
You can see the start of the clipping calculation occuring on line 118 of that source file var clipping = new CohenSutherlandClipping(clippingRectangle); - this is not something I'm particularly familiar with, but a quick wikipedia search shows that it's an algorithm used specifically for working out line clipping, there is it least on other algorithm used elswhere in that source file. I don't believe you need to alter the clipping rectangle, unless the inversion of one of the data points would place it outside the currently drawn region.
As to actually helping arrive at a solution, there are a couple of things I noticed while exploring your code. The first thing I tried was to plot some data points (all positive), and found the entire graph was inverted, essentially because this statement:
if (lpts[i].Y < threshold) is always true for positive values. That's a result of the Y axis coordinate system starting at the top of the window, and increasing toward the bottom of the window. Since the threshold in my case was 0, when translated to a rendering position on the screen, every positive data point's Y position will be smaller than the axis Y value; essentially your logic as to which points are flipped or not, requires inverting. This should get you the behaviour you're after (ensuring the flipped points are calculated correctly.)
Alternative Approach
Rather than get too deep into the clipping rectangle / calculating the transformed data points approach, I went for a slightly lazier route, which could benefit from some tidyup, but may be usefulm depending on your requirements.
I decided to perform the threshold flipping/amendment just before the call to actually render the points is made.
I altered your AbsoluteStairStepSeries class with these changes (To the Render method) in a minimal way, retaining most of your existing structure:
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);
}
}
I'm using a List<Tuple<bool, ScreenPoint>>, rather than List<ScreenPoint> to store a bool flag against each point, representing whether or not that point has been altered; you could use a small class to simplify the syntax.
Because you're interacting with the point data directly, you don't need to worry about screen position (reversed Y axis), so conceptually the calculation to take the absolute value is easier to read:
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
I notice that your code has reflect above/reflect below, which is probably logic you would insert here if required, I've gone for Math.Abs which you mentioned was your initial requirement.
When it comes to actually rendering the line, I've left the original code which draws the StepSeries in place, so actually the entire series is drawn in green. I've only added a conditional statement to check for modified/reflected points, if any are found, the relevant plot points are added to your existing lists containing flipped points, which are then drawn in red.
The Tuples make things a little messy in the render method (the addition of Item1/Item2), and you could remove the double drawing of the modified points, but I think the results are what you're after (or can certainly point you in the right direction.
Example Behaviour:

Is OnRenderSizeChanged not called properly?

I have a custom canvas that's supposed to fill itself with the maximum amount of Tile objects.
If it's resized to be bigger, it fills the empty space with additional Tiles.
If It's resized to be smaller, it removed the Tiles that are no longer visible.
This only works if I resize very slowly, otherwise it all starts to mess up.
Correct Image http://imageshack.us/a/img585/105/50186779.png
Correct Image http://imageshack.us/a/img267/3179/76648216.png
I must be obviously doing something wrong, the code below is the canvas:
using System;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1.View
{
public class TileCanvas : Canvas
{
#region Fields
private Boolean _firstCreation;
private Double _heightDifference;
private Double _widthDifference;
#endregion // Fields
#region Properties
public static readonly DependencyProperty TileSizeProperty =
DependencyProperty.Register("TileSize", typeof (Int32), typeof (TileCanvas),
new PropertyMetadata(30));
public Int32 TileSize
{
get { return (Int32) GetValue(TileSizeProperty); }
set { SetValue(TileSizeProperty, value); }
}
public Int32 Columns { get; private set; }
public Int32 Rows { get; private set; }
#endregion // Properties
#region Constructors
public TileCanvas()
{
_firstCreation = true;
}
#endregion // Constructors
#region Methods
#region Public
#endregion // Methods - Public
#region Protected
/// <summary>
/// Gets called when the rendering size of this control changes.
/// </summary>
/// <param name="sizeInfo">Information on the size change.</param>
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
Console.WriteLine("{0}x{1}", sizeInfo.NewSize.Width, sizeInfo.NewSize.Height);
_widthDifference += sizeInfo.NewSize.Width - sizeInfo.PreviousSize.Width;
_heightDifference += sizeInfo.NewSize.Height - sizeInfo.PreviousSize.Height;
int columnDifference = 0;
int rowDifference = 0;
if (_widthDifference > TileSize || _widthDifference < TileSize*-1)
{
columnDifference = ((Int32) _widthDifference)/TileSize;
Columns += columnDifference;
_widthDifference = 0;
}
if (_heightDifference > TileSize || _heightDifference < TileSize*-1)
{
rowDifference = ((Int32) _heightDifference)/TileSize;
Rows += rowDifference;
_heightDifference = 0;
}
UpdateTileSet(columnDifference, rowDifference);
}
#endregion // Methods - Protected
#region Private
/// <summary>
/// Updates the number of tiles if the control changed in size.
/// </summary>
private void UpdateTileSet(Int32 columnChange, Int32 rowChange)
{
// Exit if there's no practical change
if (columnChange == 0 && rowChange == 0)
{
return;
}
// Delete all tiles that fall out on vertical resize
if (rowChange < 0)
{
for (var row = Rows; row > Rows + rowChange; row--)
{
for (var column = 0; column < Columns; column++)
{
Children.RemoveRange(GetListIndex(0, Rows), Columns);
}
}
}
// Delete all tiles that fall out on horizontal resize
if (columnChange < 0)
{
for (var column = Columns; column > Columns + columnChange; column--)
{
for (var row = 0; row < Rows; row++)
{
Children.RemoveAt(GetListIndex(column, row));
}
}
}
// Fill new rows with tiles on vertical resize
if (rowChange > 0)
{
for (var row = Rows - rowChange; row < Rows; row++)
{
for (var column = 0; column < Columns; column++)
{
var tile = new Tile(column, row);
Point position = GetCanvasPosition(column, row);
var index = GetListIndex(column, row);
SetLeft(tile, position.X);
SetTop(tile, position.Y);
Children.Insert(index, tile);
}
}
}
// The first population is a special case that can be handled
// by filling rows only.
if (_firstCreation)
{
_firstCreation = false;
return;
}
// Fill new columns with tiles on horizontal resize
if (columnChange > 0)
{
for (var column = Columns - columnChange; column < Columns; column++)
{
for (var row = 0; row < Rows; row++)
{
var tile = new Tile(column, row);
Point position = GetCanvasPosition(column, row);
var index = GetListIndex(column, row);
SetLeft(tile, position.X);
SetTop(tile, position.Y);
Children.Insert(index, tile);
}
}
}
}
/// <summary>
/// Returns the index a tile should occupy based on its column and row.
/// </summary>
/// <param name="column">The column in which this tile resides.</param>
/// <param name="row">The row in which this tile resides.</param>
/// <returns></returns>
private Int32 GetListIndex(Int32 column, Int32 row)
{
return row*Columns + column;
}
/// <summary>
/// Returns the coordinates of a specific position.
/// </summary>
/// <param name="column">The column the position is in.</param>
/// <param name="row">The row the position is in.</param>
/// <returns></returns>
private Point GetCanvasPosition(Int32 column, Int32 row)
{
var positionX = column*TileSize;
var positionY = row*TileSize;
return new Point(positionX, positionY);
}
#endregion // Methods - Private
#endregion // Methods
}
}
This is Tile.cs
using System;
using System.Windows.Controls;
namespace WpfApplication1.View
{
/// <summary>
/// Interaction logic for Tile.xaml
/// </summary>
public partial class Tile : Border
{
public Tile(Int32 column, Int32 row)
{
InitializeComponent();
Textfield.Text = String.Format("{0}x{1}", column, row);
}
}
}
and Tile.xaml
<Border x:Class="WpfApplication1.View.Tile"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Width="40" Height="40">
<TextBlock x:Name="Textfield" FontSize="8" HorizontalAlignment="Center" VerticalAlignment="Top"/>
</Border>
Where's the issue?
Here's a good source of information about how to place elements at custom positions in a WPF control: http://codeblitz.wordpress.com/2009/03/20/wpf-auto-arrange-animated-panel/
I don't know if it applies to canvas, but placing your code inside the Measure/Arrange Override methods might yield better results.

How can I get a CheckEdit in a NavBarGroup

We need to place a check box (and caption for it) in the header of a NavBarGroup. Is there a way to do this?
We created a NavBarGroupChecked class (NavBarGroupChecked.cs) that inherits from NavBarGroup and can just be dropped in to replace it. It adds a RepositoryItemCheckEdit member that tracks the checkbox and implements custom draw. It has a Checked property that tells you if it is checked and an event that will be called when the Checked status changes. That's pretty much it. Just drops in and works.
Code is below and also downloadable here.
// built from http://www.devexpress.com/example=E2061
using System;
using System.ComponentModel;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
using DevExpress.XtraEditors.Drawing;
using DevExpress.XtraEditors.Repository;
using DevExpress.XtraEditors.ViewInfo;
using DevExpress.XtraNavBar;
using DevExpress.XtraNavBar.ViewInfo;
namespace AutoTagCore.net.windward.controls
{
/// <summary>
/// A NavBarGroup that has a check box (with caption) in its header.
/// </summary>
public class NavBarGroupChecked : NavBarGroup
{
/// <summary>
/// Occurs when the Checked property value has been changed.
/// </summary>
public event EventHandler CheckedChanged;
private const int CHECK_BOX_WIDTH = 15;
private bool isLocked;
private RepositoryItemCheckEdit _GroupEdit;
private NavBarControl _NavBarControl;
private Rectangle hotRectangle;
/// <summary>
/// Initializes a new instance of the <see cref="T:DevExpress.XtraNavBar.NavBarGroup"/> class, with the specified caption.
/// </summary>
/// <param name="caption">A string representing the NavBar group's caption.</param>
public NavBarGroupChecked(string caption)
: base(caption)
{
ctor();
}
private void ctor()
{
GroupEdit = new RepositoryItemCheckEdit { GlyphAlignment = DevExpress.Utils.HorzAlignment.Far };
GroupEdit.Appearance.Options.UseTextOptions = true;
GroupEdit.Appearance.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Far;
GroupEdit.GlyphAlignment = DevExpress.Utils.HorzAlignment.Far;
ItemChanged += NavBarGroupChecked_ItemChanged;
}
private void NavBarGroupChecked_ItemChanged(object sender, System.EventArgs e)
{
if (NavBar != NavBarControl)
NavBarControl = NavBar;
}
/// <summary>
/// Creates an instance of the <see cref="T:DevExpress.XtraNavBar.NavBarGroup"/> class.
/// </summary>
public NavBarGroupChecked()
{
ctor();
}
/// <summary>
/// The NavBarControl that owns this. This must be set to work.
/// </summary>
private NavBarControl NavBarControl
{
get { return _NavBarControl; }
set { UnsubscribeEvents(value); _NavBarControl = value; SubscribeEvents(value); }
}
private void SubscribeEvents(NavBarControl navBarControl)
{
if (navBarControl == null)
return;
NavBarControl.CustomDrawGroupCaption += NavBarControl_CustomDrawGroupCaption;
NavBarControl.MouseClick += NavBarControl_MouseClick;
}
private void UnsubscribeEvents(NavBarControl navBarControl)
{
if (navBarControl != null)
return;
NavBarControl.CustomDrawGroupCaption -= NavBarControl_CustomDrawGroupCaption;
NavBarControl.MouseClick -= NavBarControl_MouseClick;
}
/// <summary>
/// true if the box is checked.
/// </summary>
public bool Checked { get; set; }
/// <summary>
/// The indent of the check box for the end of the header.
/// </summary>
public int CheckIndent { get; set; }
///<summary>
/// The check box displayed in the header.
///</summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RepositoryItemCheckEdit GroupEdit
{
get { return _GroupEdit; }
set { _GroupEdit = value; }
}
private Rectangle GetCheckBoxBounds(Rectangle fixedCaptionBounds)
{
return new Rectangle(fixedCaptionBounds.Right - CHECK_BOX_WIDTH - CheckIndent, fixedCaptionBounds.Top, CHECK_BOX_WIDTH, fixedCaptionBounds.Height);
}
private bool IsCustomDrawNeeded(NavBarGroup group)
{
return GroupEdit != null && NavBarControl != null && !isLocked && group == this;
}
private void NavBarControl_CustomDrawGroupCaption(object sender, CustomDrawNavBarElementEventArgs e)
{
NavGroupInfoArgs infoArgs = (NavGroupInfoArgs) e.ObjectInfo;
if (!IsCustomDrawNeeded(infoArgs.Group))
return;
try
{
isLocked = true;
BaseNavGroupPainter painter = NavBarControl.View.CreateGroupPainter(NavBarControl);
Rectangle checkBoxBounds = GetCheckBoxBounds(infoArgs.CaptionBounds);
painter.DrawObject(infoArgs);
DrawCheckBox(e.Graphics, checkBoxBounds);
e.Handled = true;
}
finally
{
isLocked = false;
}
}
private void DrawCheckBox(Graphics g, Rectangle r)
{
BaseEditPainter painter = GroupEdit.CreatePainter();
BaseEditViewInfo info = GroupEdit.CreateViewInfo();
info.EditValue = Checked;
SizeF textBounds = info.Appearance.CalcTextSize(g, GroupEdit.Caption, 500);
int totalWidth = (int)textBounds.Width + r.Width + 10;
info.Bounds = new Rectangle(r.Right - totalWidth, r.Y, totalWidth, r.Height);
info.CalcViewInfo(g);
ControlGraphicsInfoArgs args = new ControlGraphicsInfoArgs(info, new DevExpress.Utils.Drawing.GraphicsCache(g), r);
painter.Draw(args);
args.Cache.Dispose();
}
private static NavBarViewInfo GetNavBarView(NavBarControl NavBar)
{
PropertyInfo pi = typeof(NavBarControl).GetProperty("ViewInfo", BindingFlags.Instance | BindingFlags.NonPublic);
return pi.GetValue(NavBar, null) as NavBarViewInfo;
}
private bool IsCheckBox(Point p)
{
NavBarHitInfo hi = NavBarControl.CalcHitInfo(p);
if (hi.Group == null || hi.Group != this)
return false;
NavBarViewInfo vi = GetNavBarView(NavBarControl);
vi.Calc(NavBarControl.ClientRectangle);
NavGroupInfoArgs groupInfo = vi.GetGroupInfo(hi.Group);
Rectangle checkBounds = GetCheckBoxBounds(groupInfo.CaptionBounds);
hotRectangle = checkBounds;
return checkBounds.Contains(p);
}
private void NavBarControl_MouseClick(object sender, MouseEventArgs e)
{
if (!IsCheckBox(e.Location))
return;
Checked = !Checked;
NavBarControl.Invalidate(hotRectangle);
if (CheckedChanged != null)
CheckedChanged(sender, e);
}
}
}
I had some issues with the way that solution worked and made some minor tweaks. See below.
A sample on how to use this is;
private void Form1_Load(object sender, EventArgs e)
{
var item = navBarControl1.Groups.Add(new NavBarGroupChecked("NavBarGroupCheckbox", navBarControl1)) as NavBarGroupChecked;
item.Hint = "my hint";
item.CheckedChanged += checkchanged;
}
private void checkchanged(object sender, EventArgs e)
{
MessageBox.Show("It Changed");
}
This is the solution:
// built from http://www.devexpress.com/example=E2061
using System;
using System.ComponentModel;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
using DevExpress.XtraEditors.Drawing;
using DevExpress.XtraEditors.Repository;
using DevExpress.XtraEditors.ViewInfo;
using DevExpress.XtraNavBar;
using DevExpress.XtraNavBar.ViewInfo;
namespace NavBarCheckTest
{
/// <summary>
/// A NavBarGroup that has a check box (with caption) in its header.
/// </summary>
public class NavBarGroupChecked : NavBarGroup
{
/// <summary>
/// Occurs when the Checked property value has been changed.
/// </summary>
public event EventHandler CheckedChanged;
private const int CHECK_BOX_WIDTH = 15;
private bool isLocked;
private RepositoryItemCheckEdit _GroupEdit;
private NavBarControl _NavBarControl;
private Rectangle hotRectangle;
/// <summary>
/// Initializes a new instance of the <see cref="T:DevExpress.XtraNavBar.NavBarGroup"/> class, with the specified caption.
/// </summary>
/// <param name="caption">A string representing the NavBar group's caption.</param>
public NavBarGroupChecked(string caption, NavBarControl parent = null)
: base(caption)
{
ctor(parent);
}
private void ctor(NavBarControl parent = null)
{
GroupEdit = new RepositoryItemCheckEdit { GlyphAlignment = DevExpress.Utils.HorzAlignment.Far };
GroupEdit.Caption = string.Empty;
GroupEdit.Appearance.Options.UseTextOptions = true;
GroupEdit.Appearance.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Far;
GroupEdit.GlyphAlignment = DevExpress.Utils.HorzAlignment.Far;
ItemChanged += NavBarGroupChecked_ItemChanged;
if (parent != null)
NavBarControl = parent;
}
private void NavBarGroupChecked_ItemChanged(object sender, System.EventArgs e)
{
if (NavBar != NavBarControl)
NavBarControl = NavBar;
}
/// <summary>
/// Creates an instance of the <see cref="T:DevExpress.XtraNavBar.NavBarGroup"/> class.
/// </summary>
public NavBarGroupChecked(NavBarControl parent = null)
{
ctor(parent);
}
/// <summary>
/// The NavBarControl that owns this. This must be set to work.
/// </summary>
private NavBarControl NavBarControl
{
get { return _NavBarControl; }
set { UnsubscribeEvents(value); _NavBarControl = value; SubscribeEvents(value); }
}
private void SubscribeEvents(NavBarControl navBarControl)
{
if (navBarControl == null)
return;
NavBarControl.CustomDrawGroupCaption += NavBarControl_CustomDrawGroupCaption;
NavBarControl.MouseClick += NavBarControl_MouseClick;
}
private void UnsubscribeEvents(NavBarControl navBarControl)
{
if (navBarControl != null)
return;
NavBarControl.CustomDrawGroupCaption -= NavBarControl_CustomDrawGroupCaption;
NavBarControl.MouseClick -= NavBarControl_MouseClick;
}
/// <summary>
/// true if the box is checked.
/// </summary>
public bool Checked { get; set; }
/// <summary>
/// The indent of the check box for the end of the header.
/// </summary>
public int CheckIndent { get; set; }
///<summary>
/// The check box displayed in the header.
///</summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RepositoryItemCheckEdit GroupEdit
{
get { return _GroupEdit; }
set { _GroupEdit = value; }
}
private int GetCheckBoxWidth()
{
return CheckIndent * 2 + 10;
}
//private Rectangle GetCaptionBounds(Rectangle originalCaptionBounds)
//{
// return new Rectangle(originalCaptionBounds.Location, new Size(originalCaptionBounds.Width - GetCheckBoxWidth(), originalCaptionBounds.Height));
//}
private Rectangle GetCheckBoxBounds(Rectangle fixedCaptionBounds)
{
return new Rectangle(fixedCaptionBounds.Right - CHECK_BOX_WIDTH - CheckIndent, fixedCaptionBounds.Top, CHECK_BOX_WIDTH, fixedCaptionBounds.Height);
//return new Rectangle(fixedCaptionBounds.Right, fixedCaptionBounds.Top, GetCheckBoxWidth(), fixedCaptionBounds.Height); ;
}
private bool IsCustomDrawNeeded(NavBarGroup group)
{
return GroupEdit != null && NavBarControl != null && !isLocked && group == this;
}
private void NavBarControl_CustomDrawGroupCaption(object sender, CustomDrawNavBarElementEventArgs e)
{
NavGroupInfoArgs infoArgs = (NavGroupInfoArgs) e.ObjectInfo;
if (!IsCustomDrawNeeded(infoArgs.Group))
return;
try
{
isLocked = true;
BaseNavGroupPainter painter = NavBarControl.View.CreateGroupPainter(NavBarControl);
Rectangle originalCaptionBounds = new Rectangle(infoArgs.CaptionClientBounds.X, infoArgs.CaptionClientBounds.Y, infoArgs.CaptionClientBounds.Width - infoArgs.ButtonBounds.Width, infoArgs.CaptionClientBounds.Height);
Rectangle checkBoxBounds = GetCheckBoxBounds(originalCaptionBounds);
painter.DrawObject(infoArgs);
DrawCheckBox(e.Graphics, checkBoxBounds);
e.Handled = true;
}
finally
{
isLocked = false;
}
}
private void DrawCheckBox(Graphics g, Rectangle r)
{
BaseEditPainter painter = GroupEdit.CreatePainter();
BaseEditViewInfo info = GroupEdit.CreateViewInfo();
info.EditValue = Checked;
SizeF textBounds = info.Appearance.CalcTextSize(g, GroupEdit.Caption, 500);
int totalWidth = (int)textBounds.Width + r.Width + 10;
info.Bounds = new Rectangle(r.Right - totalWidth, r.Y, totalWidth, r.Height);
info.CalcViewInfo(g);
ControlGraphicsInfoArgs args = new ControlGraphicsInfoArgs(info, new DevExpress.Utils.Drawing.GraphicsCache(g), r);
painter.Draw(args);
args.Cache.Dispose();
}
private static NavBarViewInfo GetNavBarView(NavBarControl NavBar)
{
PropertyInfo pi = typeof(NavBarControl).GetProperty("ViewInfo", BindingFlags.Instance | BindingFlags.NonPublic);
return pi.GetValue(NavBar, null) as NavBarViewInfo;
}
private bool IsCheckBox(Point p)
{
NavBarHitInfo hi = NavBarControl.CalcHitInfo(p);
if (hi.Group == null || hi.Group != this)
return false;
NavBarViewInfo vi = GetNavBarView(NavBarControl);
vi.Calc(NavBarControl.ClientRectangle);
NavGroupInfoArgs groupInfo = vi.GetGroupInfo(hi.Group);
Rectangle originalCaptionBounds = new Rectangle(groupInfo.CaptionClientBounds.X, groupInfo.CaptionClientBounds.Y, groupInfo.CaptionClientBounds.Width - groupInfo.ButtonBounds.Width, groupInfo.CaptionClientBounds.Height);
Rectangle checkBounds = GetCheckBoxBounds(originalCaptionBounds);
hotRectangle = checkBounds;
return checkBounds.Contains(p);
}
private void NavBarControl_MouseClick(object sender, MouseEventArgs e)
{
if (!IsCheckBox(e.Location))
return;
Checked = !Checked;
NavBarControl.Invalidate(hotRectangle);
if (CheckedChanged != null)
CheckedChanged(sender, e);
}
}
}

How can I implement a commandable ColumnSeries in the WPF Toolkit's Chart Control

I need to be able to specify a command to run when the SelectionChanged event fires. I already know how to implement the ICommandSource interface; what I need to know is how I can just add a command to the column series to handle the SelectionChanged event.
When I inherit from the ColumnBarBaseSeries<...> base class I have to override GetAxes() and UpdateDatePoint() which I am not sure how to implement.
You can use attached behaviours to solve this problem.
Create a SelectionChangedBehaviour that wires a selectionChanged event to the element that you're attaching the behaviour to then you can binding any ICommand to that behaviour.
For more on attached behaviours -
Introduction article by Josh Smith
Overview of the concept
Another good introduction
Hope that helps
Here is some code to add an attached behavior to a ColumnSeries for a SelectionChanged Command.
public static class ColumnSeriesBehavior
{
private static DelegateCommand<object> SelectionChangedCommand;
public static DelegateCommand<object> GetSelectionChangedCommand(ColumnSeries cs)
{
return cs.GetValue(SelectionChangedCommandProperty) as DelegateCommand<object>;
}
public static void SetSelectionChangedCommand(ColumnSeries cs, DelegateCommand<object> value)
{
cs.SetValue(SelectionChangedCommandProperty, value);
}
// Using a DependencyProperty as the backing store for SelectionChangedCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectionChangedCommandProperty =
DependencyProperty.RegisterAttached("SelectionChangedCommand", typeof(DelegateCommand<object>), typeof(ColumnSeriesBehavior), new UIPropertyMetadata(null, OnSelectionChangedCommandChanged));
private static void OnSelectionChangedCommandChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
ColumnSeries item = depObj as ColumnSeries;
if (item == null)
{
return;
}
if (e.NewValue is DelegateCommand<object> == false)
{
return;
}
SelectionChangedCommand = e.NewValue as DelegateCommand<object>;
item.SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(Column_SelectionChanged);
}
private static void Column_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (SelectionChangedCommand != null)
SelectionChangedCommand.Execute(sender);
}
}
And in the XAML to attach the property:
<chartingToolkit:Chart.Series>
<chartingToolkit:ColumnSeries
IsSelectionEnabled="True"
ItemsSource="{Binding YourItemSource}"
IndependentValueBinding="{Binding YourIndValue, Path=YourIndValuePath}"
DependentValueBinding="{Binding YourDepValue, Path=YourDepValuePath}"
>
<chartingToolkit:ColumnSeries.Style>
<Style>
<!-- Attaching the SelectionChangedCommand behavior -->
<Setter Property="local:ColumnSeriesBehavior.SelectionChangedCommand"
Value="{Binding YourDelegateCommand}"/>
</Style>
</chartingToolkit:ColumnSeries.Style>
</chartingToolkit:ColumnSeries>
</chartingToolkit:Chart.Series>
Here is some code that appears to work for me to implement your own CommandColumnSeries, I stole a lot of it from the source for the ColumnSeries Sealed Class:
public class CommandColumnSeries : ColumnBarBaseSeries<ColumnDataPoint>
{
#region "ICommandSource"
[Localizability(LocalizationCategory.NeverLocalize), Category("Action"), Bindable(true)]
public ICommand Command
{
get
{
return (ICommand)base.GetValue(CommandProperty);
}
set
{
base.SetValue(CommandProperty, value);
}
}
[Bindable(true), Category("Action"), Localizability(LocalizationCategory.NeverLocalize)]
public object CommandParameter
{
get
{
return base.GetValue(CommandParameterProperty);
}
set
{
base.SetValue(CommandParameterProperty, value);
}
}
[Category("Action"), Bindable(true)]
public IInputElement CommandTarget
{
get
{
return (IInputElement)base.GetValue(CommandTargetProperty);
}
set
{
base.SetValue(CommandTargetProperty, value);
}
}
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(CommandColumnSeries), new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandColumnSeries), new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register("CommandTarget", typeof(IInputElement), typeof(CommandColumnSeries), new FrameworkPropertyMetadata(null));
#endregion
#region public IRangeAxis DependentRangeAxis
/// <summary>
/// Gets or sets the dependent range axis.
/// </summary>
public IRangeAxis DependentRangeAxis
{
get { return GetValue(DependentRangeAxisProperty) as IRangeAxis; }
set { SetValue(DependentRangeAxisProperty, value); }
}
/// <summary>
/// Identifies the DependentRangeAxis dependency property.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "This member is necessary because the base classes need to share this dependency property.")]
public static readonly DependencyProperty DependentRangeAxisProperty =
DependencyProperty.Register(
"DependentRangeAxis",
typeof(IRangeAxis),
typeof(ColumnSeries),
new PropertyMetadata(null, OnDependentRangeAxisPropertyChanged));
/// <summary>
/// DependentRangeAxisProperty property changed handler.
/// </summary>
/// <param name="d">ColumnBarBaseSeries that changed its DependentRangeAxis.</param>
/// <param name="e">Event arguments.</param>
private static void OnDependentRangeAxisPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CommandColumnSeries source = (CommandColumnSeries)d;
IRangeAxis newValue = (IRangeAxis)e.NewValue;
source.OnDependentRangeAxisPropertyChanged(newValue);
}
/// <summary>
/// DependentRangeAxisProperty property changed handler.
/// </summary>
/// <param name="newValue">New value.</param>
private void OnDependentRangeAxisPropertyChanged(IRangeAxis newValue)
{
this.InternalDependentAxis = (IAxis)newValue;
}
#endregion public IRangeAxis DependentRangeAxis
#region public IAxis IndependentAxis
/// <summary>
/// Gets or sets the independent category axis.
/// </summary>
public IAxis IndependentAxis
{
get { return GetValue(IndependentAxisProperty) as IAxis; }
set { SetValue(IndependentAxisProperty, value); }
}
/// <summary>
/// Identifies the IndependentAxis dependency property.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "This member is necessary because the base classes need to share this dependency property.")]
public static readonly DependencyProperty IndependentAxisProperty =
DependencyProperty.Register(
"IndependentAxis",
typeof(IAxis),
typeof(ColumnSeries),
new PropertyMetadata(null, OnIndependentAxisPropertyChanged));
/// <summary>
/// IndependentAxisProperty property changed handler.
/// </summary>
/// <param name="d">ColumnBarBaseSeries that changed its IndependentAxis.</param>
/// <param name="e">Event arguments.</param>
private static void OnIndependentAxisPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CommandColumnSeries source = (CommandColumnSeries)d;
IAxis newValue = (IAxis)e.NewValue;
source.OnIndependentAxisPropertyChanged(newValue);
}
/// <summary>
/// IndependentAxisProperty property changed handler.
/// </summary>
/// <param name="newValue">New value.</param>
private void OnIndependentAxisPropertyChanged(IAxis newValue)
{
this.InternalIndependentAxis = (IAxis)newValue;
}
#endregion public IAxis IndependentAxis
public CommandColumnSeries()
{
this.SelectionChanged += new SelectionChangedEventHandler(CommandColumnSeries_SelectionChanged);
}
private void CommandColumnSeries_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (Command != null)
{
RoutedCommand routedCommand = Command as RoutedCommand;
CommandParameter = e.Source;
if (routedCommand != null)
{
routedCommand.Execute(CommandParameter, CommandTarget);
}
else
{
Command.Execute(CommandParameter);
}
}
}
protected override void GetAxes(DataPoint firstDataPoint)
{
// Taken from the source of the ColumnSeries sealed class.
GetAxes(
firstDataPoint,
(axis) => axis.Orientation == AxisOrientation.X,
() => new CategoryAxis { Orientation = AxisOrientation.X },
(axis) =>
{
IRangeAxis rangeAxis = axis as IRangeAxis;
return rangeAxis != null && rangeAxis.Origin != null && axis.Orientation == AxisOrientation.Y;
},
() =>
{
IRangeAxis rangeAxis = CreateRangeAxisFromData(firstDataPoint.DependentValue);
rangeAxis.Orientation = AxisOrientation.Y;
if (rangeAxis == null || rangeAxis.Origin == null)
{
throw new InvalidOperationException("No Suitable Axes found for plotting range axis.");
}
DisplayAxis axis = rangeAxis as DisplayAxis;
if (axis != null)
{
axis.ShowGridLines = true;
}
return rangeAxis;
});
}
protected override void UpdateDataPoint(DataPoint dataPoint)
{
// This code taken from the ColumnSeries sealed class.
if (SeriesHost == null )//|| PlotArea == null)
{
return;
}
object category = dataPoint.ActualIndependentValue ?? (IndexOf<DataPoint>(this.ActiveDataPoints, 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<CommandColumnSeries> columnSeries = SeriesHost.Series.OfType<CommandColumnSeries>().Where(series => series.ActualIndependentAxis == ActualIndependentAxis);
int numberOfSeries = columnSeries.Count();
double coordinateRangeWidth = (maximum - minimum);
double segmentWidth = coordinateRangeWidth * 0.8;
double columnWidth = segmentWidth / numberOfSeries;
int seriesIndex = IndexOf<CommandColumnSeries>(columnSeries, this);
double dataPointY = ActualDependentRangeAxis.GetPlotAreaCoordinate(ToDouble(dataPoint.ActualDependentValue)).Value.Value;
double zeroPointY = ActualDependentRangeAxis.GetPlotAreaCoordinate(ActualDependentRangeAxis.Origin).Value.Value;
double offset = 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 = GroupIndexOf(categoryGrouping, dataPoint);
dataPointX += (index * (columnWidth * 0.2)) / (categoryGrouping.Count() - 1);
columnWidth *= 0.8;
Canvas.SetZIndex(dataPoint, -index);
}
if (CanGraph(dataPointY) && CanGraph(dataPointX) && CanGraph(zeroPointY))
{
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;
}
}
private static int IndexOf<T>(IEnumerable<T> collection, T target)
{
int i = 0;
foreach (var obj in collection)
{
if (obj.Equals(target))
return i;
i++;
}
return -1;
}
private static int GroupIndexOf(IGrouping<object, DataPoint> group, DataPoint point)
{
int i = 0;
foreach (var pt in group)
{
if (pt == point)
return i;
i++;
}
return -1;
}
/// <summary>
/// Returns a value indicating whether this value can be graphed on a
/// linear axis.
/// </summary>
/// <param name="value">The value to evaluate.</param>
/// <returns>A value indicating whether this value can be graphed on a
/// linear axis.</returns>
private static bool CanGraph(double value)
{
return !double.IsNaN(value) && !double.IsNegativeInfinity(value) && !double.IsPositiveInfinity(value) && !double.IsInfinity(value);
}
/// <summary>
/// Converts an object into a double.
/// </summary>
/// <param name="value">The value to convert to a double.</param>
/// <returns>The converted double value.</returns>
private static double ToDouble(object value)
{
return Convert.ToDouble(value, CultureInfo.InvariantCulture);
}
}

Resources