+2

Individual column/bar selection.

bkboggy 9 years ago updated 9 years ago 6
I've been trying to figure this out all night and haven't managed to find any code or any discussions to assist me with this one.  Similar discussions were focused on points in scatter and line charts, but nothing regarding columns or bars.

I have a requirement of being able to select individual columns in a column chart to access related data; however, none of my attempts at handling selection changed or mouse down produced any viable results.  SelectionChanged event doesn't even fire for me and no visible selection happens.  SelectionColor for the model does not appear to do anything and SelectionMode doesn't produce any visible behavior with selection enabled in the ColumnSeries.
I should add that if it's not possible to select individual columns with the current API, is there a way to get at the underlying item in the series?
Does it have Preview*  events? If so, try to subscribe to PreviewMouseDown maybe that would help... 
Good point, Mike.  It doesn't have Preview version of events, but MouseDown does work.  Here's what I was able to do and it works for getting the underlying item data:

void columns_MouseDown(object sender, OxyMouseDownEventArgs e)  <br>{  <br>    var columns = sender as ColumnSeries;  <br>    if (columns != null)  <br>    {  <br>        var nearest = columns.GetNearestPoint(e.Position, false);  <br>        var data = nearest.Item as Item;  <br>        if (data != null)  <br>        {  <br>            MessageBox.Show(data.Value.ToString(CultureInfo.InvariantCulture)); <br>        }  <br>    }  <br>}
So, that works great for getting the data, which was my major requirement.  However, I can't seem figure out how to interact with the actual column, to change its color, so that there is a feedback for the user regarding their selection.  My column series looks like this:

var columns = new ColumnSeries  <br>{  <br>    ItemsSource = Items,  <br>    ValueField = "Value",  <br>    StrokeThickness = 1,  <br>    LabelPlacement = LabelPlacement.Inside,  <br>    LabelFormatString = "{0}",  <br>    TextColor = OxyColor.FromArgb(255, 255, 255, 255),  <br>    FillColor = OxyColor.FromArgb(200, 0, 121, 211),  <br>    Selectable = true,  <br>    SelectionMode = SelectionMode.Single,  <br>    TrackerFormatString = "{1}: {Value}"  <br>};<br>
Since selectable or selection mode does nothing (it appears), I need to find a work-around for getting the column itself and changing its color.

I would think that I'd be able to get at it using the Items property, but it doesn't show any items. I'm able to get the source item based on the nearest point, but not its ColumnItem. ActualItems is protected, so I can't get columns out of that:

Alright, I was actually able to figure out a solution.  Instead of binding source to some arbitrary item model, I derived my Item from ColumnItem, so that I may have direct control over every column I place into ColumnSeries:

public class Item : ColumnItem  
{  
    public string ID { get; set; }
    public string Label { get; set; } 
}
Adding the Label allows me to bind it to the CategoryAxis. Likewise, ID is used for internal data identification and linking.  So, then, I simply wrapped my data into this new object and passed it to the ColumnSeries' Items property by using the AddRange() method.

I changed the previous code that got the item and its data to look at Items and the ColumnItem instead:

void columns_MouseDown(object sender, OxyMouseDownEventArgs e) 
{
    var series = sender as ColumnSeries;
    if (series != null)
    {
        var nearest = series.GetNearestPoint(e.Position, false);
        var column = nearest.Item as ColumnItem;
        if (column != null) 
        {
            column.Color = OxyColors.Green;
            series.PlotModel.InvalidatePlot(true);  
        }
    } 
}
Obviously, the color changing during selection has not been developed much, since I'm out of time today, but it's very easy to track selection, so this is a good basis for anyone else who ran into the same issue as I.  Basically, all you have to do is track a SelectedItem, which is null by default, but then simply point to whatever item you click on.  When a new click happens, you change the existing SelectedItem, if it's not null, to the default state, and make the new item as the selected one. And to get the underlying data, beyond the value, I would cast to Item instead of ColumnItem.

For reusability, since this scenario comes around very often in my projects, I'll probably derive a new class from ColumnSeries that does all of that by default.
I took a bit of a break of my Trig homework, so I was able to type up a wrapper for the ColumnSeries and the ColumnItem that simplify the process.  I won't have enough time tonight to create a fork for this project, so below is the code for anyone that's interested (let me know if I should make any changes): http://pastebin.com/vaLWMJ2k
I worked on it some more to clean things up and to make the whole thing a bit more robust.  I've added a custom event as well, so that it's possible to subscribe to the column selection change, rather than having to subscribe to PropertyChanged and then figuring out if it's a column change... waste of resources.  So, here's the code (let me know if there may be any improvements made): http://pastebin.com/gt4F19AX

I tried making it more generic, using BarSeriesBase and BarItemBase, but that requires a bit more work and I am dead tired.

Note that I'm using MVVM Light framework, so that's where ViewModelBase comes from.

Here's the code again, just in case pastebin link is broken.

    /// <summary>
/// SelectedColumnChanged event delegate.
/// </summary>
/// <param name="sender">Object triggering the event.</param>
/// <param name="e">Event arguments.</param>
public delegate void SelectedColumnChangedEventHandler(object sender, SelectedColumnChangedEventArgs e);

/// <summary>
/// SelectedColumnChanged event arguments. Contains the old and the newly
/// selected column.
/// </summary>
public class SelectedColumnChangedEventArgs : EventArgs
{
/// <summary>
/// Old selected column.
/// </summary>
public ColumnItem OldColumn { get; private set; }

/// <summary>
/// New selected column.
/// </summary>
public ColumnItem NewColumn { get; private set; }

/// <summary>
/// Constructor.
/// </summary>
/// <param name="oldColumn">Old selected column.</param>
/// <param name="newColumn">New selected column.</param>
public SelectedColumnChangedEventArgs(ColumnItem oldColumn, ColumnItem newColumn)
{
OldColumn = oldColumn;
NewColumn = newColumn;
}

}

/// <summary>
/// Extended OxyPlot ColumnSeries class. Allows selection of columns and tracks which
/// column has been selected.
/// </summary>
public class SelectableColumnSeries : ColumnSeries, INotifyPropertyChanged
{
/// <summary>
/// Selected ColumnItem field.
/// </summary>
private ColumnItem _selectedColumn;

/// <summary>
/// Selected ColumnItem property.
/// </summary>
public ColumnItem SelectedColumn
{
get { return _selectedColumn; }
set
{
if (_selectedColumn != value)
{
var changeArgs = new SelectedColumnChangedEventArgs(_selectedColumn, value);
_selectedColumn = value;
NotifyPropertyChanged();
NotifySelectedColumnChanged(changeArgs);
}
}
}

/// <summary>
/// Selected ColumnItem color field.
/// </summary>
private OxyColor _selectedColumnColor;

/// <summary>
/// Selected ColumnItem color property.
/// </summary>
public OxyColor SelectedColumnColor
{
get { return _selectedColumnColor; }
set
{
_selectedColumnColor = value;
NotifyPropertyChanged();
}
}

/// <summary>
/// SelectableColumnSeries constructor.
/// </summary>
public SelectableColumnSeries()
{
MouseDown += SelectableColumnSeries_MouseDown;
}

/// <summary>
/// MouseDown event handler for the SelectableColumnSeries. Handles changing of the selected
/// ColumnItem, as well as its color, if a SelectedColumnColor was provided. If the color for
/// selection was not provided, then no visual feedback is provided, but the selection is still
/// made.
/// </summary>
/// <param name="sender">Event sender (object).</param>
/// <param name="e">Event arguments (OxyMouseDownEventArgs).</param>
protected void SelectableColumnSeries_MouseDown(object sender, OxyMouseDownEventArgs e)
{
var nearest = GetNearestPoint(e.Position, false);
var column = nearest.Item as ColumnItem;
if (column == null)
{
return;
}

if (SelectedColumn != null)
{
SelectedColumn.Color = FillColor;
}

if (!SelectedColumnColor.IsUndefined())
{
column.Color = SelectedColumnColor;
}

SelectedColumn = column;
}

/// <summary>
/// PropertyChanged event handler.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// PropertyChanged event handling method.
/// </summary>
/// <param name="propertyName">Property name. If not passed, name of the property,
/// which called this method, is used.</param>
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PlotModel != null)
{
PlotModel.InvalidatePlot(true);
}

if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

/// <summary>
/// SelectedColumnChanged event handler.
/// </summary>
public event SelectedColumnChangedEventHandler SelectedColumnChanged;

/// <summary>
/// SelectedColumnChanged handling method.
/// </summary>
/// <param name="e">Event arguments. Includes old column and the newly selected column.</param>
protected virtual void NotifySelectedColumnChanged(SelectedColumnChangedEventArgs e)
{
if (SelectedColumnChanged != null)
{
SelectedColumnChanged(this, e);
}
}
}

/// <summary>
/// Column graph view model.
/// </summary>
public class ColumnGraphViewModel : ViewModelBase
{
/// <summary>
/// Graph item observable collection field.
/// </summary>
private ObservableCollection<ExtendedColumnItem> _items;
/// <summary>
/// Graph item observable collection property.
/// </summary>
public ObservableCollection<ExtendedColumnItem> Items
{
get { return _items; }
set { Set(() => Items, ref _items, value); }
}

/// <summary>
/// Plot model field.
/// </summary>
private PlotModel _model;
/// <summary>
/// Plot model property.
/// </summary>
public PlotModel Model
{
get { return _model; }
set { Set(() => Model, ref _model, value); }
}

/// <summary>
/// Linear axis field. This is the axis on the left side, which
/// provides a reference for numerical values.
/// </summary>
private LinearAxis _valueAxis;
/// <summary>
/// Linear axis property. This is the axis on the left side, which
/// provides a reference for numerical values.
/// </summary>
public LinearAxis ValueAxis
{
get { return _valueAxis; }
set { Set(() => ValueAxis, ref _valueAxis, value); }
}

/// <summary>
/// Category axis field. This is the axis on the bottom, which
/// provides categories for the columns.
/// </summary>
private CategoryAxis _bottomCategoryAxis;
/// <summary>
/// Category axis property. This is the axis on the bottom, which
/// provides categories for the columns.
/// </summary>
public CategoryAxis BottomCategoryAxis
{
get { return _bottomCategoryAxis; }
set { Set(() => BottomCategoryAxis, ref _bottomCategoryAxis, value); }
}

/// <summary>
/// Column series field.
/// </summary>
private SelectableColumnSeries _series;
/// <summary>
/// Column series property.
/// </summary>
public SelectableColumnSeries Series
{
get { return _series; }
set { Set(() => Series, ref _series, value); }
}

/// <summary>
/// Default constructor.
/// </summary>
public ColumnGraphViewModel(List<ExtendedColumnItem> items,
string graphTitle = "", string valueAxisTitle = "", string categoryAxisTitle = "")
{
Initialize(items, graphTitle, valueAxisTitle, categoryAxisTitle);
}

/// <summary>
/// Initializes all of the properties. May be overridden.
/// </summary>
private void Initialize(List<ExtendedColumnItem> items,
string graphTitle = "", string valueAxisTitle = "", string categoryAxisTitle = "")
{
Items = new ObservableCollection<ExtendedColumnItem>(items);

Model = new PlotModel
{
Title = graphTitle,
TitleColor = OxyColor.FromArgb(200, 0, 0, 0),
IsLegendVisible = false
};

BottomCategoryAxis = new CategoryAxis
{
Title = categoryAxisTitle,
AxisTitleDistance = 10,
TitleFontSize = 13,
ItemsSource = Items,
LabelField = "Label",
IsZoomEnabled = false,
IsPanEnabled = false,
GapWidth = .2

};
Model.Axes.Add(BottomCategoryAxis);

ValueAxis = new LinearAxis
{
Title = valueAxisTitle,
AxisTitleDistance = 10,
TitleFontSize = 13,
Position = AxisPosition.Left,
MinimumPadding = 0,
AbsoluteMinimum = 0,
IsZoomEnabled = false,
IsPanEnabled = false
};
Model.Axes.Add(ValueAxis);

Series = new SelectableColumnSeries
{
ValueField = "Value",
StrokeThickness = 1,
LabelPlacement = LabelPlacement.Inside,
LabelFormatString = "{0}",
TextColor = OxyColor.FromArgb(255, 255, 255, 255),
FillColor = OxyColor.FromArgb(200, 0, 121, 211),
SelectedColumnColor = OxyColor.FromArgb(200, 76, 179, 255),
TrackerFormatString = "{1}: {Value}"
};
Series.Items.AddRange(Items);
Model.Series.Add(Series);
}
}