Skip to content

Instantly share code, notes, and snippets.

@dodikk
Last active January 29, 2018 11:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dodikk/78a592aa00de56e877ad938034f44f13 to your computer and use it in GitHub Desktop.
Save dodikk/78a592aa00de56e877ad938034f44f13 to your computer and use it in GitHub Desktop.
[issue] OxyPlot columns and line series together (oxyplot #1187)
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
};
}
}
}
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
}
}
@dodikk
Copy link
Author

dodikk commented Jan 29, 2018

@dodikk
Copy link
Author

dodikk commented Jan 29, 2018

Oxyplot issue oxyplot/oxyplot#1187

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment