I have been working on a workaround to some of the issues I have been having with bar series graphs, and feel like I have gotten as close as I can, but I can't nail down whatever the issue is that I am having... in the 6.5.1 release of the software there is a problem with having one x axis item have 2 bars, and another having 3 for example... not to dump a huge amount of code on here, but this is the algorithm I have come up with for doing this.... I tried to comment it the best I could....
Also, to reference the other outstanding issue is that the team I am working on doesn't want to upgrade to the latest version for several reasons, so that is why I am trying to find a workaround...
Code: Select all
private void PlotEntries(List<StateTopResourcesDataEntry> entries, List<StateResourceType> excludedResources = null)
{
Chart.BeginUpdate();
//Get XYview
var chartView = Chart.ViewXY;
// Clear all current bars on the plot
chartView.BarSeries.Clear();
// Group all bars together by location
chartView.BarViewOptions.Grouping = BarsGrouping.ByLocation;
chartView.BarViewOptions.BarSpacing = 0;
// Clear the last set of custom ticks
var axisX = chartView.XAxes[0];
axisX.CustomTicks.Clear();
//Get default y-axis and set the range.
var axisY = chartView.YAxes[0];
// Order the plots by state name
var plotPoints = entries.OrderBy(e => e.StateName);
// Grab all the states to get a unique state list
var uniqueStates = plotPoints.Select(e => e.StateName);
// Create the custom ticks based on the state names (these positions will be modified later below)
// For now, just place them on the graph
var plotCount = 0;
foreach (var state in uniqueStates)
{
axisX.CustomTicks.Add(new CustomAxisTick {AxisValue = plotCount++, LabelText = state});
}
// Place the x axis labels at 90 degrees
axisX.LabelsAngle = 90;
// Has to be done so the custom ticks will redraw
axisX.InvalidateCustomTicks();
var allBars = new List<BarSeries>();
var stateBarDictionary = new List<BarSeriesWithIndex>();
var stateIndex = 0;
// In order to support a different number of bars per x axis point, we need to add the items in a round-robin fashion...
// That is, we need to add the plot point from item 1 first, then the first item from 2, then from 3, etc...
// Then, we need to go back and plot the next item from 1, the next item from 2, etc... we keep doing this until we run
// out of buckets
foreach (var plotPoint in plotPoints)
{
// Each plot point represents the state containing its resources
var barSeries = new List<BarSeries>();
// Loop through the resources in each state...
foreach (var resource in plotPoint.Resources)
{
// In case there are excluded resources, don't apply them
if (null == excludedResources || !excludedResources.Contains(resource.ResourceType))
{
// Create the bar and tag it with the resource type for later
var bar = ItemFactory.CreateBarSeries(chartView, axisX, axisY);
bar.Tag = resource.ResourceType;
// This text dictates how the legend shows up
bar.Title.Text = resource.ResourceType.ToString();
// If this is already being shown, make sure it doesn't show up in the legend twice!
if (!_currentResourcesShown.Contains(resource.ResourceType))
{
bar.ShowInLegendBox = true;
_currentResourcesShown.Add(resource.ResourceType);
}
else
{
bar.ShowInLegendBox = false;
}
bar.BorderColor = Colors.DarkGray;
bar.BorderWidth = 1;
// Fill color based on resource type
switch (resource.ResourceType)
{
case StateResourceType.Banking:
bar.Fill = new Fill() {Color = Colors.Green};
break;
case StateResourceType.Logging:
bar.Fill = new Fill() {Color = Colors.DarkOliveGreen};
break;
case StateResourceType.Mining:
bar.Fill = new Fill() {Color = Colors.Goldenrod};
break;
case StateResourceType.Oil:
bar.Fill = new Fill() {Color = Colors.Black};
break;
case StateResourceType.Tech:
bar.Fill = new Fill() {Color = Colors.Crimson};
break;
case StateResourceType.Tourism:
bar.Fill = new Fill() {Color = Colors.DarkMagenta};
break;
default:
bar.Fill = new Fill() {Color = Colors.Transparent};
break;
}
// Find the custom tick associated with this item (the state's name)
var tick = axisX.CustomTicks.SingleOrDefault(t => t.LabelText == plotPoint.StateName);
// Create a bar for this specific item, giving it the location, text and values
if (tick != null)
{
bar.Values = new[]
{
new BarSeriesValue()
{
Location = stateIndex,
Text = resource.IncomeFrom.ToString(),
Value = resource.IncomeFrom,
}
};
barSeries.Add(bar);
}
}
}
// Done adding bar values for all items in this state, add in bar series with index...
if (barSeries.Count > 0)
{
stateBarDictionary.Add(new BarSeriesWithIndex(barSeries));
}
++stateIndex;
}
var keepGoing = true;
var index = 0;
var totalItems = stateBarDictionary.Count();
while(totalItems > 0 && keepGoing)
{
// Get the item at the current index (0, 1, 2, ...)
var item = stateBarDictionary[index];
// Reset this for the check below (to see if all items have been exhausted)
var itemCount = 0;
// If the current item we are looking has exhausted all the bar series made for it, look for the next item that hasn't yet..
while(item.CurrentIndex >= item.BarSeries.Count())
{
// This means we have checked all items and they have all exhausted their bar series... exit out
if (itemCount == totalItems)
{
keepGoing = false;
break;
}
// Go to the next item and check to see if all items have been used in it...
index = (index + 1) % totalItems;
item = stateBarDictionary[index];
++itemCount;
}
// True means we found an item in our bucket where we haven't added all the bars into the series
if (keepGoing)
{
// Add this bar next...
allBars.Add(item.BarSeries[item.CurrentIndex]);
// Next time we come to this item, we will add the next bar from it
item.CurrentIndex++;
index = (index + 1)%totalItems;
}
}
// Add the bar series in the order it was created above... this is the workaround for Arction not working like it should
allBars.ForEach(l => chartView.BarSeries.Add(l));
// This will cause all the bars to regraph
allBars.ForEach(l => l.InvalidateData());
// Determine the ranges manually for the bar graphs
DetermineGraphRanges(stateIndex);
// Now, we have to space the bars correctly ourselves (something else Arction doesn't seem to do correctly)
SpaceBars(stateIndex);
// Now, we have to position our bars correctly ourselves (something else Arction doesn't seem to do correctly)
PositionBarsCorrectly(stateIndex);
Chart.ViewXY.LegendBox.SeriesTitleMouseClick += SeriesTitleClickHandler;
Chart.ViewXY.LegendBox.CheckBoxStateChanged += CheckBoxStateChangedHandler;
/*
var chartView = Chart.ViewXY;
var axisX = chartView.XAxes[0];
var axisY = chartView.YAxes[0];
chartView.BarViewOptions.Grouping = BarsGrouping.ByLocation;
chartView.BarViewOptions.BarSpacing = 0;
var bar = ItemFactory.CreateBarSeries(chartView, axisX, axisY);
bar.Title.Text = "Dummy";
bar.ShowInLegendBox = true;
bar.Fill = new Fill() { Color = Colors.Green };
bar.Values = new[]
{
new BarSeriesValue()
{
Location = 1.0,
Text = "1000",
Value = 1000,
}
};
chartView.BarSeries.Add(bar);*/
Chart.EndUpdate();
}
private void DetermineGraphRanges(int currentStateCount)
{
// Find the bar spacing (in our case, bar spacing will be the spacing between groups of bars, not individual bars)
var barSpacing = Chart.ViewXY.BarViewOptions.BarSpacing;
// Find the minimum custom tick location
var minimumCustomTickLocation = Chart.ViewXY.XAxes[0].CustomTicks.Min(ct => ct.AxisValue);
// Find the maximum custom tick location
var maximumCustomTickLocation = Chart.ViewXY.XAxes[0].CustomTicks.Max(ct => ct.AxisValue);
// The min/max region we can draw in will take into account the bar spacing, so we give enough room on the left and right
// of the chart
var minScreenCoord =
Chart.ViewXY.XAxes[0].ValueToCoord(minimumCustomTickLocation) - barSpacing;
var maxScreenCoord =
Chart.ViewXY.XAxes[0].ValueToCoord(maximumCustomTickLocation) + barSpacing;
// We need to convert these values back to value points
double minValueCoord;
double maxValueCoord;
if (Chart.ViewXY.XAxes[0].CoordToValue((int)minScreenCoord, out minValueCoord, false))
{
Chart.ViewXY.XAxes[0].Minimum = minValueCoord;
}
if (Chart.ViewXY.XAxes[0].CoordToValue((int)maxScreenCoord, out maxValueCoord, false))
{
Chart.ViewXY.XAxes[0].Maximum = maxValueCoord;
}
}
private void SpaceBars(int currentStateCount)
{
// The min/max region we can draw in will take into account the bar spacing, so we give enough room on the left and right
// of the chart
var minScreenCoord =
Chart.ViewXY.XAxes[0].ValueToCoord(Chart.ViewXY.XAxes[0].Minimum);
var maxScreenCoord =
Chart.ViewXY.XAxes[0].ValueToCoord(Chart.ViewXY.XAxes[0].Maximum);
// Actual region we can draw in is this... the difference between the min and max screen coordinate
// This would be for one bar, for instance
var barWidth = (maxScreenCoord - minScreenCoord);
// Find number of ticks graphed...
var numberOfTicksGraphed = Chart.ViewXY.XAxes[0].CustomTicks.Count;
// Find the bar spacing (in our case, bar spacing will be the spacing between groups of bars, not individual bars)
var barSpacing = Chart.ViewXY.BarViewOptions.BarSpacing;
// Take out the region we have for graphing based on the bar spacing and the number of items graphed...
// We will use the spacing only between tick marks and groups of bars
// Arction doesn't provide a way to space bars by grouping... we have to do this manually
barWidth -= (barSpacing * (numberOfTicksGraphed - 1));
// Count the total number of bars
float barCount = Chart.ViewXY.BarSeries.Count(s => _currentResourcesShown.Contains((StateResourceType)(int)s.Tag));
// Find what the bar width should be
barWidth /= barCount;
// Set all the bar widths
foreach (var barSeries in Chart.ViewXY.BarSeries)
{
barSeries.BarThickness = (int)barWidth;
}
}
private void PositionBarsCorrectly(int currentStateCount)
{
var barSpacing = Chart.ViewXY.BarViewOptions.BarSpacing;
// Find current screen coordinate...
var currentScreenCoordinate = Chart.ViewXY.XAxes[0].ValueToCoord(Chart.ViewXY.XAxes[0].Minimum) + barSpacing;
// All bars are same thickness... just look at the first one
var barThickness = Chart.ViewXY.BarSeries[0].BarThickness;
// Figure out how many bars associated with the current x item... we can order the bar series by location
foreach(var customTick in Chart.ViewXY.XAxes[0].CustomTicks)
//for(int stateIndex = 0; stateIndex < currentStateCount; ++stateIndex)
{
var currentGrouping = Chart.ViewXY.BarSeries.Where(barSeries => Math.Abs(barSeries.Values[0].Location - customTick.AxisValue) < float.Epsilon).ToList();
// Count number of items in this grouping...
var count = currentGrouping.Count();
// Total width of this region....
float totalWidth = count*barThickness;
// Find the middle of this region (the tick needs to go in the middle of the bar group)
var correctBarLocation = currentScreenCoordinate + (totalWidth / 2);
// Find end of this region in screen coordinates...
currentScreenCoordinate += totalWidth;
// Now, convert to value coordinate, and all items in this grouping will get updated to that position...
var valueCoordinate = 0.0;
if (Chart.ViewXY.XAxes[0].CoordToValue((int)correctBarLocation, out valueCoordinate, true))
{
// Need to update the custom tick location first...
customTick.AxisValue = valueCoordinate;
// Update all the bar series to this location (there is only one value per series...)
foreach (var barSeries in currentGrouping)
{
barSeries.Values[0].Location = valueCoordinate;
}
}
// Move to the next beginning of the bar (which should be based on the bar spacing...
currentScreenCoordinate += barSpacing;
}
}
Here is a reference to the display I get using the code...
[img] [/img]
Thanks for your time!