Last active
January 29, 2018 11:49
-
-
Save dodikk/78a592aa00de56e877ad938034f44f13 to your computer and use it in GitHub Desktop.
[issue] OxyPlot columns and line series together (oxyplot #1187)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PracticeDashboard.Helpers | |
{ | |
using DAL.Models; | |
using DAL.Models.Dashboard; | |
using Models; | |
using global::OxyPlot; | |
using global::OxyPlot.Axes; | |
using global::OxyPlot.Series; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Infrastructure.Extensions; | |
using OxyPlot.Extensions; | |
public static class PlotHelpers | |
{ | |
public const int DefaultMarkerSize = 3; | |
public const int DefaultThickness = 3; | |
public const int LeftRightOffset = 15; | |
public static DateTimeAxis GetDateTimeAxis( | |
DateTime start, | |
DateTime end, | |
DateTime absoluteStart, | |
DateTime absoluteEnd, | |
OxyColor textColor) | |
{ | |
TimeSpan timeDiffForData = (end - start); | |
// TODO: possible issue. | |
// Not all months have 30 days. | |
// | |
var eightMonths = TimeSpan.FromDays(30 * 8); | |
TimeSpan onScreenTimeInterval = eightMonths; | |
bool wholePlotOnScreen = (timeDiffForData <= onScreenTimeInterval); | |
// == | |
// | |
DateTime eightMonthsBeforeEnd = end.AddMonths(-8); | |
DateTime eightMonthsAfterStart = start.AddMonths(8); | |
// == | |
// | |
Double fStart = DateTimeAxis.ToDouble(start); | |
Double fEnd = DateTimeAxis.ToDouble(end); | |
Double fAbsoluteStart = DateTimeAxis.ToDouble(absoluteStart); | |
Double fAbsoluteEnd = DateTimeAxis.ToDouble(absoluteEnd); | |
Double fEightMonthsAfterStart = DateTimeAxis.ToDouble(eightMonthsAfterStart); | |
Double fEightMonthsBeforeEnd = DateTimeAxis.ToDouble(eightMonthsBeforeEnd); | |
// == | |
// | |
var minimumNoMargin = | |
wholePlotOnScreen | |
? fStart | |
: fEightMonthsBeforeEnd; | |
var minimum = minimumNoMargin - LeftRightOffset; | |
// == | |
// | |
var maximumNoMargin = | |
wholePlotOnScreen | |
? fEightMonthsAfterStart | |
: fEnd; | |
var maximum = maximumNoMargin + LeftRightOffset; | |
return new DateTimeAxis | |
{ | |
Position = AxisPosition.Bottom, | |
IntervalType = DateTimeIntervalType.Months, | |
StringFormat = "MMM", | |
IntervalLength = 30.5, | |
TicklineColor = OxyColors.Transparent, | |
TextColor = textColor, | |
IsZoomEnabled = false, | |
IsAxisVisible = true, | |
Minimum = minimum, | |
Maximum = maximum, | |
AbsoluteMinimum = fAbsoluteStart - LeftRightOffset, | |
AbsoluteMaximum = fAbsoluteEnd + LeftRightOffset, | |
}; | |
} | |
public static LinearAxis GetLinearAxis( | |
double minimum, | |
double maximum, | |
OxyColor textColor, | |
double? gridStep = null, // TODO: maybe revert to `int?` type | |
AxisPosition position = AxisPosition.Right, | |
bool moneyValues = false) | |
{ | |
double unwrappedStep = gridStep ?? maximum; | |
const double zeroPrecision = 0.001; | |
bool isZeroStep = (Math.Abs(unwrappedStep) < zeroPrecision); | |
var majorStep = | |
isZeroStep | |
? double.MaxValue | |
: (gridStep ?? maximum); | |
var extractedMaximum = | |
maximum > minimum | |
? maximum + (maximum * 0.1) | |
: double.MaxValue; | |
var absoluteMaximum = extractedMaximum; | |
var resultMaximum = extractedMaximum; | |
Func<double, string> labelFormatterLambda = | |
(val) => | |
moneyValues | |
? $"${val.ShortFormat()}" // adding dollar sign | |
: val.ShortFormat(); | |
var result = new LinearAxis | |
{ | |
Position = position, | |
TickStyle = TickStyle.Inside, | |
TicklineColor = OxyColor.FromArgb(0, 0, 0, 0), | |
TextColor = textColor, | |
MajorStep = majorStep, | |
LabelFormatter = labelFormatterLambda, | |
IsZoomEnabled = false, | |
IsPanEnabled = false, | |
AbsoluteMinimum = minimum, | |
AbsoluteMaximum = absoluteMaximum, | |
Minimum = minimum, | |
Maximum = resultMaximum | |
}; | |
return result; | |
} | |
public static CategoryAxis GetCategoryAxis( | |
List<DateTime> items, | |
OxyColor textColor) | |
{ | |
return new CategoryAxis | |
{ | |
Position = AxisPosition.Bottom, | |
ItemsSource = items, | |
StringFormat = "MMM", | |
TicklineColor = OxyColors.Transparent, | |
TextColor = textColor, | |
IsZoomEnabled = false, | |
IsPanEnabled = false, | |
}; | |
} | |
public static LineSeries GetLineSeries( | |
List<DataPoint> data, | |
OxyColor color, | |
int markerSize, | |
int thickness, | |
bool filled) | |
{ | |
return new LineSeries | |
{ | |
MarkerSize = markerSize, | |
MarkerFill = filled ? color : OxyColors.White, | |
MarkerStroke = color, | |
MarkerStrokeThickness = thickness - (thickness / 3), | |
MarkerType = MarkerType.Circle, | |
Color = color, | |
StrokeThickness = thickness, | |
}.AddPointsCollection(data); | |
} | |
public static ColumnSeries GetColumnSeries(List<DataModel> data, OxyColor color) | |
{ | |
return new ColumnSeries | |
{ | |
StrokeColor = OxyColors.Transparent, | |
StrokeThickness = 0, | |
ColumnWidth = 4, | |
ItemsSource = data, | |
ValueField = "Value", | |
FillColor = color | |
}; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PracticeDashboard.PlotBuilders.SalesPipeline | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Helpers; | |
using global::OxyPlot; | |
using global::OxyPlot.Axes; | |
using global::OxyPlot.Series; | |
using PracticeDashboard.DAL.Models.SalesPipeline; | |
using Money = System.Decimal; | |
using System.Diagnostics; | |
using PracticeDashboard.Models; | |
public class SalesPipelineNormalizedPlotBuilder | |
{ | |
private readonly SalesPipelinePlotTheme currentTheme = SalesPipelinePlotTheme.DefaultTheme(); | |
private SalesPipelineChartModel pipelineData; | |
private SalesPipelineModelNormalizationHelper normalizedData; | |
public SalesPipelineNormalizedPlotBuilder() | |
{ | |
} | |
#region ISalesPipelinePlotBuilder | |
public PlotModel ResultModel { get; private set; } | |
public SalesPipelineNormalizedPlotBuilder WithReport(SalesPipelineChartModel pipelineData) | |
{ | |
Debug.Assert(null == this.pipelineData); | |
this.pipelineData = pipelineData; | |
this.GenerateNormalizedData(); | |
return this; | |
} | |
public SalesPipelineNormalizedPlotBuilder Build() | |
{ | |
Debug.Assert(null != this.pipelineData); | |
this.ImplBuildPlot(); | |
Debug.Assert(null != this.ResultModel); | |
return this; | |
} | |
#endregion ISalesPipelinePlotBuilder | |
private void ImplBuildPlot() | |
{ | |
PlotModel result = new PlotModel(); | |
this.AddAxisForBarsToPlot(result); | |
this.AddTimeAxisToPlot(result); | |
this.AddVerticalAxisToPlot(result); | |
this.AddCountLineToPlot(result); | |
this.AddAmountLineSeriesToPlot(result); | |
this.AddCountBarsToPlot(result); | |
this.ResultModel = result; | |
} | |
#region logic | |
private void GenerateNormalizedData() | |
{ | |
Debug.Assert(null != this.pipelineData); | |
this.normalizedData = new SalesPipelineModelNormalizationHelper(); | |
this.normalizedData.Normalize( | |
salesPipelineDataset: this.pipelineData, | |
majorTickCount: this.currentTheme.numberOfSquaresInGrid); | |
} | |
#endregion logic | |
#region series | |
/** | |
* | |
* Adds "total count" data as it is. | |
* | |
*/ | |
private void AddCountLineToPlot(PlotModel result) | |
{ | |
Func<SalesPipelineDataPoint, DataPoint> dataPointBuilder = | |
point => | |
new DataPoint( | |
x: DateTimeAxis.ToDouble(point.yearAndMonth), | |
y: Convert.ToDouble(point.pendingOpportunitiesTotalCount) | |
); | |
List<DataPoint> points = | |
pipelineData.ValuesByMonth.Select(dataPointBuilder) | |
.ToList(); | |
LineSeries lineSeries = PlotHelpers.GetLineSeries( | |
data: points, | |
color: this.currentTheme.salesCountLineColor, | |
markerSize: PlotHelpers.DefaultMarkerSize, | |
thickness: PlotHelpers.DefaultThickness, | |
filled: false); | |
result.Series.Add(lineSeries); | |
} | |
/** | |
* | |
* Adds data of "amounts" normalized to "count space" | |
* | |
* | |
*/ | |
private void AddAmountLineSeriesToPlot(PlotModel result) | |
{ | |
Func<SalesPipelineDataPointNormalized, DataPoint> dataPointBuilder = | |
point => | |
new DataPoint( | |
x: DateTimeAxis.ToDouble(point.yearAndMonth), | |
y: point.totalBudgetAmountForPendingOpportunities | |
); | |
List<DataPoint> points = | |
this.normalizedData | |
.emulatedAmountsDataset | |
.ValuesByMonth.Select(dataPointBuilder) | |
.ToList(); | |
LineSeries lineSeries = PlotHelpers.GetLineSeries( | |
data: points, | |
color: this.currentTheme.amountsLineColor, | |
markerSize: PlotHelpers.DefaultMarkerSize, | |
thickness: PlotHelpers.DefaultThickness, | |
filled: false); | |
result.Series.Add(lineSeries); | |
} | |
#endregion series | |
#region Bars | |
/** | |
* | |
* Adds tripple bar "count" data as it is. | |
* | |
*/ | |
private void AddCountBarsToPlot(PlotModel result) | |
{ | |
var dataset = this.normalizedData.emulatedAmountsDataset.ValuesByMonth; | |
// == | |
// | |
var startedBarPlotData = | |
dataset.Select( | |
salesPipelineDataPoint => | |
new OxyColumnViewModel | |
{ | |
Date = salesPipelineDataPoint.yearAndMonth, | |
Value = salesPipelineDataPoint.numberOfOpportunitiesStartedThisMonth, | |
Color = this.currentTheme.newCountBarColor | |
}); | |
var startedBarSeries = | |
new ColumnSeries | |
{ | |
StrokeColor = OxyColors.Transparent, | |
StrokeThickness = 0, | |
ColumnWidth = 4, | |
ItemsSource = startedBarPlotData, | |
ValueField = "Value", | |
ColorField = "Color" | |
}; | |
// == | |
// | |
var wonBarPlotData = | |
dataset.Select( | |
salesPipelineDataPoint => | |
new OxyColumnViewModel | |
{ | |
Date = salesPipelineDataPoint.yearAndMonth, | |
Value = salesPipelineDataPoint.numberOfOpportunitiesWonThisMonth, | |
Color = this.currentTheme.wonCountBarColor | |
}); | |
var wonBarSeries = | |
new ColumnSeries | |
{ | |
StrokeColor = OxyColors.Transparent, | |
StrokeThickness = 0, | |
ColumnWidth = 4, | |
ItemsSource = wonBarPlotData, | |
ValueField = "Value", | |
ColorField = "Color" | |
}; | |
// == | |
// | |
var lostBarPlotData = | |
dataset.Select( | |
salesPipelineDataPoint => | |
new OxyColumnViewModel | |
{ | |
Date = salesPipelineDataPoint.yearAndMonth, | |
Value = salesPipelineDataPoint.numberOfOpportunitiesLostThisMonth, | |
Color = this.currentTheme.lostCountBarColor | |
}); | |
var lostBarSeries = | |
new ColumnSeries | |
{ | |
StrokeColor = OxyColors.Transparent, | |
StrokeThickness = 0, | |
ColumnWidth = 4, | |
ItemsSource = lostBarPlotData, | |
ValueField = "Value", | |
ColorField = "Color" | |
}; | |
result.Series.Add(startedBarSeries); | |
result.Series.Add(wonBarSeries); | |
result.Series.Add(lostBarSeries); | |
} | |
private void AddAxisForBarsToPlot(PlotModel result) | |
{ | |
var dataset = this.normalizedData.emulatedAmountsDataset.ValuesByMonth; | |
// CategoryAxis is forced by OxyPlot if ColumnSeries are used | |
// otherwise an exception is thrown | |
// | |
var dates = | |
dataset.Select( | |
salesPipelineDataPoint => salesPipelineDataPoint.yearAndMonth | |
); | |
var columnAxisX = new CategoryAxis | |
{ | |
Position = AxisPosition.Bottom, | |
ItemsSource = dates, | |
StringFormat = "MMM", | |
TicklineColor = OxyColors.Transparent, | |
TextColor = this.currentTheme.axisColor, | |
IsZoomEnabled = false, | |
IsPanEnabled = true, | |
}; | |
this.ConfigureGridForTimeAxis(columnAxisX); | |
result.Axes.Add(columnAxisX); | |
} | |
#endregion Bars | |
#region Axes | |
private void AddTimeAxisToPlot(PlotModel result) | |
{ | |
var dateTimeAxis = this.AddMonthTitlesToPlot(result); | |
this.AddVerticalGridLinesToPlot( | |
result, | |
withDateTimeAxis: dateTimeAxis); | |
} | |
private DateTimeAxis AddMonthTitlesToPlot(PlotModel result) | |
{ | |
// == date axis | |
// | |
// assuming `pipelineData.ValuesByMonth` is sorted by date ascending | |
// otherwise min/max lookup should be implemented | |
// | |
var minDate = pipelineData.ValuesByMonth.First().yearAndMonth; | |
var maxDate = pipelineData.ValuesByMonth.Last().yearAndMonth; | |
var minDateWithOffset = minDate.AddMonths(-1); | |
var maxDateWithOffset = maxDate.AddMonths(+1); | |
var timeAxis = | |
PlotHelpers.GetDateTimeAxis( | |
start: minDate, | |
end: maxDate, | |
absoluteStart: minDateWithOffset, | |
absoluteEnd: maxDateWithOffset, | |
textColor: OxyColor.FromRgb(r: 255, g: 0, b: 0)); | |
result.Axes.Add(timeAxis); | |
return timeAxis; | |
} | |
private void AddVerticalGridLinesToPlot( | |
PlotModel result, | |
DateTimeAxis withDateTimeAxis) | |
{ | |
DateTimeAxis dateTimeAxis = withDateTimeAxis; | |
var shiftedDates = this.normalizedData.shiftedDatetimeTicks; | |
var shiftedDatesInAxisSpace = | |
shiftedDates.Select( | |
singleDateTime => DateTimeAxis.ToDouble(singleDateTime) | |
); | |
dateTimeAxis.ExtraGridlines = shiftedDatesInAxisSpace.ToArray(); | |
dateTimeAxis.ExtraGridlineThickness = 2; | |
dateTimeAxis.ExtraGridlineStyle = LineStyle.Dash; | |
dateTimeAxis.ExtraGridlineColor = OxyColor.FromRgb(r: 255, g: 0, b: 0); | |
} | |
private void AddVerticalAxisToPlot(PlotModel result) | |
{ | |
int countMaxValue = this.normalizedData.countMax; | |
double fCountMaxValue = this.normalizedData.fCountMax; | |
double emulatedAmountMaxValue = this.normalizedData.emulatedAmountMax; | |
double fNumberOfSquaresInGrid = Convert.ToDouble(this.currentTheme.numberOfSquaresInGrid); | |
double step = this.normalizedData.countStep; | |
double maxValue = Math.Max(fCountMaxValue, emulatedAmountMaxValue) + step; | |
var verticalAxis = | |
PlotHelpers.GetLinearAxis( | |
minimum: 0, | |
maximum: maxValue, | |
textColor: this.currentTheme.axisColor, | |
gridStep: step, | |
position: AxisPosition.Right, | |
moneyValues: false); | |
var previousFormatter = verticalAxis.LabelFormatter; | |
verticalAxis.LabelFormatter = | |
(double arg) => | |
{ | |
string currentLabel = previousFormatter(arg); | |
double precision = 0.001; | |
bool isZeroAmount = | |
Math.Abs(arg - this.normalizedData.emulatedAmountZeroOffset) <= precision; | |
bool isCountLabel = | |
(arg < this.normalizedData.emulatedAmountZeroOffset); | |
if (isZeroAmount) | |
{ | |
return "$0"; | |
} | |
else if (isCountLabel) | |
{ | |
string lambdaResult = currentLabel; | |
return lambdaResult; | |
} | |
else | |
{ | |
double recoveredAmount = | |
this.normalizedData.ConvertEmulatedAmountToOriginalScale(arg); | |
double millionsCount = | |
recoveredAmount / 1000000; | |
string strMillions = millionsCount.ToString("N1"); | |
string lambdaResult = $"${strMillions}M"; | |
return lambdaResult; | |
} | |
}; | |
this.ConfigureGridForVerticalAxis(verticalAxis); | |
result.Axes.Add(verticalAxis); | |
} | |
#endregion Axes | |
#region Grid | |
private void ConfigureGridForVerticalAxis(Axis verticalAxis) | |
{ | |
verticalAxis.MajorGridlineStyle = LineStyle.Solid; | |
verticalAxis.MajorGridlineThickness = 1; | |
var blackColor = OxyColor.FromRgb(r: 0, g: 0, b: 0); | |
verticalAxis.MajorGridlineColor = blackColor; | |
} | |
private void ConfigureGridForTimeAxis(Axis timeAxis) | |
{ | |
timeAxis.MajorGridlineStyle = LineStyle.Dash; | |
timeAxis.MajorGridlineThickness = 1; | |
var blackColor = OxyColor.FromRgb(r: 0, g: 0, b: 0); | |
timeAxis.MajorGridlineColor = blackColor; | |
} | |
#endregion Grid | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Solved https://gist.github.com/dodikk/02e85b3f38dc4bd87cf96a489a0e2983