How to: Custom Draw Series Points
- 7 minutes to read
This example demonstrates one of several possible ways of using the ChartControl.CustomDrawSeriesPoint event. In the sample, the event is used to draw a custom legend markers of series points.
A custom legend marker is set to the CustomDrawSeriesEventArgsBase.LegendMarkerImage property. Note that in this case, the CustomDrawSeriesEventArgsBase.DisposeLegendMarkerImage property should be set to true to avoid memory leaks.
To customize options used to draw the series point, cast CustomDrawSeriesEventArgsBase.SeriesDrawOptions to the DrawOptions class descendant that stores draw options of the required series view type.
Note
A complete sample project is available at https://github.com/DevExpress-Examples/how-to-draw-a-custom-legend-marker-for-a-series-point-t332652
namespace CustomSeriesPointDrawingSample.Model {
using System.Data.Entity;
public partial class NwindDbContext : DbContext {
public NwindDbContext()
: base("name=NwindDbContext") {
}
public virtual DbSet<Employee> Employees { get; set; }
public virtual DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
}
}
}
using CustomSeriesPointDrawingSample.Model;
using DevExpress.XtraCharts;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace CustomSeriesPointDrawingSample {
public partial class Form1 : Form {
object trackedPointArgument;
Dictionary<string, Image> photoCache = new Dictionary<string, Image>();
#region #Constants
const int borderSize = 5;
const int scaledPhotoWidth = 48;
const int scaledPhotoHeight = 51;
// Width and height of scaled photo with border.
const int totalWidth = 58;
const int totalHeight = 61;
// Rects required to create a custom legend series marker.
static readonly Rectangle photoRect = new Rectangle(
borderSize, borderSize,
scaledPhotoWidth, scaledPhotoHeight);
static readonly Rectangle totalRect = new Rectangle(
0, 0,
totalWidth, totalHeight);
#endregion
public Form1() {
InitializeComponent();
}
#region #ChartPreparation
private void Form1_Load(object sender, EventArgs e) {
chart.CustomDrawSeriesPoint += OnCustomDrawSeriesPoint;
chart.BoundDataChanged += OnBoundDataChanged;
chart.ObjectHotTracked += OnObjectHotTracked;
using (var context = new NwindDbContext()) {
chart.DataSource = PrepareDataSource(context.Orders);
InitPhotoCache(context.Employees);
}
chart.SeriesDataMember = "Year";
chart.SeriesTemplate.ArgumentDataMember = "Employee";
chart.SeriesTemplate.ValueDataMembers.AddRange("Value");
chart.SeriesTemplate.ToolTipPointPattern = "{S}: {A} ({VP:P})";
chart.SeriesTemplate.SeriesPointsSorting = SortingMode.Ascending;
}
#endregion
#region #AutogeneratedSeriesModifying
private void OnBoundDataChanged(object sender, EventArgs e) {
if (chart.Series.Count <= 1) return;
for (int i = 1; i < chart.Series.Count; ++i)
chart.Series[i].ShowInLegend = false;
}
#endregion
#region #CustomPointDrawing
private void OnCustomDrawSeriesPoint(object sender, CustomDrawSeriesPointEventArgs e) {
// Design a series marker image.
Bitmap image = new Bitmap(totalWidth, totalHeight);
bool isSelected = trackedPointArgument != null && e.SeriesPoint.Argument.Equals(trackedPointArgument);
using (Graphics graphics = Graphics.FromImage(image)) {
Brush fillBrush = isSelected ?
(Brush)new HatchBrush(
HatchStyle.DarkDownwardDiagonal,
e.LegendDrawOptions.Color,
e.LegendDrawOptions.ActualColor2) :
(Brush)new SolidBrush(e.LegendDrawOptions.Color);
graphics.FillRectangle(fillBrush, totalRect);
Image photo;
if (photoCache.TryGetValue(e.SeriesPoint.Argument, out photo))
graphics.DrawImage(photo, photoRect);
}
e.LegendMarkerImage = image;
e.DisposeLegendMarkerImage = true;
PieDrawOptions options = e.SeriesDrawOptions as PieDrawOptions;
if (isSelected && options != null) {
options.FillStyle.FillMode = DevExpress.XtraCharts.FillMode.Hatch;
((HatchFillOptions)options.FillStyle.Options).HatchStyle = HatchStyle.DarkDownwardDiagonal;
}
}
#endregion
private void OnObjectHotTracked(object sender, HotTrackEventArgs e) {
trackedPointArgument = e.HitInfo.InSeriesPoint ? e.HitInfo.SeriesPoint.Argument : null;
chart.Invalidate();
}
void InitPhotoCache(IEnumerable<Employee> employees) {
photoCache.Clear();
foreach (var employee in employees) {
MemoryStream stream = new MemoryStream(employee.Photo);
if (!photoCache.ContainsKey(employee.FullName))
photoCache.Add(employee.FullName, Image.FromStream(stream));
}
}
List<SalesPoint> PrepareDataSource(IEnumerable<Order> orders) {
var query = from o in orders
group o by new {
Year = o.OrderDate.Year,
Employee = o.Employee.FirstName + " " + o.Employee.LastName
}
into g
select new {
Employee = g.Key.Employee,
Year = g.Key.Year,
Values = g.Select(o => o.Freight.HasValue ? o.Freight.Value : 0)
};
List<SalesPoint> points = new List<SalesPoint>();
foreach (var item in query) {
points.Add(new SalesPoint {
Employee = item.Employee,
Year = item.Year,
Value = item.Values.Aggregate((d1, d2) => d1 + d2)
});
}
return points;
}
}
}
class SalesPoint {
public string Employee { get; set; }
public int Year { get; set; }
public decimal Value { get; set; }
}
namespace CustomSeriesPointDrawingSample.Model {
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public partial class Order
{
[Key]
[Required]
[Column("OrderID")]
public int OrderId { get; set; }
[ForeignKey("Employee")]
[Column("EmployeeID")]
public int? EmployeeId { get; set; }
public DateTime OrderDate { get; set; }
[Column(TypeName = "smallmoney")]
public decimal? Freight { get; set; }
public virtual Employee Employee { get; set; }
}
}
namespace CustomSeriesPointDrawingSample.Model {
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public partial class Employee
{
[Key]
[Required]
[Column("EmployeeID")]
public int EmployeeId { get; set; }
[Required]
[StringLength(20)]
public string LastName { get; set; }
[Required]
[StringLength(10)]
public string FirstName { get; set; }
[Column(TypeName = "image")]
public byte[] Photo { get; set; }
public string FullName {
get { return FirstName + " " + LastName; }
}
public virtual ICollection<Order> Orders { get; set; }
}
}
Imports System.Data.Entity
Namespace CustomSeriesPointDrawingSample.Model
Partial Public Class NwindDbContext
Inherits DbContext
Public Sub New()
MyBase.New("name=NwindDbContext")
End Sub
Public Overridable Property Employees() As DbSet(Of Employee)
Public Overridable Property Orders() As DbSet(Of Order)
Protected Overrides Sub OnModelCreating(ByVal modelBuilder As DbModelBuilder)
End Sub
End Class
End Namespace
Imports System.ComponentModel.DataAnnotations.Schema
Imports System.ComponentModel.DataAnnotations
Imports System
Namespace CustomSeriesPointDrawingSample.Model
Partial Public Class Order
<Key, Required, Column("OrderID")> _
Public Property OrderId() As Integer
<ForeignKey("Employee"), Column("EmployeeID")> _
Public Property EmployeeId() As Integer?
Public Property OrderDate() As Date
<Column(TypeName := "smallmoney")> _
Public Property Freight() As Decimal?
Public Overridable Property Employee() As Employee
End Class
End Namespace
Imports CustomSeriesPointDrawingSample.Model
Imports DevExpress.XtraCharts
Imports System
Imports System.Collections.Generic
Imports System.Data
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.IO
Imports System.Linq
Imports System.Windows.Forms
Namespace CustomSeriesPointDrawingSample
Partial Public Class Form1
Inherits Form
Private trackedPointArgument As Object
Private photoCache As New Dictionary(Of String, Image)()
#Region "#Constants"
Private Const borderSize As Integer = 5
Private Const scaledPhotoWidth As Integer = 48
Private Const scaledPhotoHeight As Integer = 51
' Width and height of scaled photo with border.
Private Const totalWidth As Integer = 58
Private Const totalHeight As Integer = 61
' Rects required to create a custom legend series marker.
Private Shared ReadOnly photoRect As New Rectangle(borderSize, borderSize, scaledPhotoWidth, scaledPhotoHeight)
Private Shared ReadOnly totalRect As New Rectangle(0, 0, totalWidth, totalHeight)
#End Region
Public Sub New()
InitializeComponent()
End Sub
#Region "#ChartPreparation"
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
AddHandler chart.CustomDrawSeriesPoint, AddressOf OnCustomDrawSeriesPoint
AddHandler chart.BoundDataChanged, AddressOf OnBoundDataChanged
AddHandler chart.ObjectHotTracked, AddressOf OnObjectHotTracked
Using context = New NwindDbContext()
chart.DataSource = PrepareDataSource(context.Orders)
InitPhotoCache(context.Employees)
End Using
chart.SeriesDataMember = "Year"
chart.SeriesTemplate.ArgumentDataMember = "Employee"
chart.SeriesTemplate.ValueDataMembers.AddRange("Value")
chart.SeriesTemplate.ToolTipPointPattern = "{S}: {A} ({VP:P})"
chart.SeriesTemplate.SeriesPointsSorting = SortingMode.Ascending
End Sub
#End Region
#Region "#AutogeneratedSeriesModifying"
Private Sub OnBoundDataChanged(ByVal sender As Object, ByVal e As EventArgs)
If chart.Series.Count <= 1 Then
Return
End If
For i As Integer = 1 To chart.Series.Count - 1
chart.Series(i).ShowInLegend = False
Next i
End Sub
#End Region
#Region "#CustomPointDrawing"
Private Sub OnCustomDrawSeriesPoint(ByVal sender As Object, ByVal e As CustomDrawSeriesPointEventArgs)
' Design a series marker image.
Dim image As New Bitmap(totalWidth, totalHeight)
Dim isSelected As Boolean = trackedPointArgument IsNot Nothing AndAlso e.SeriesPoint.Argument.Equals(trackedPointArgument)
Using graphics As Graphics = System.Drawing.Graphics.FromImage(image)
Dim fillBrush As Brush = If(isSelected, CType(New HatchBrush(HatchStyle.DarkDownwardDiagonal, e.LegendDrawOptions.Color, e.LegendDrawOptions.ActualColor2), Brush), CType(New SolidBrush(e.LegendDrawOptions.Color), Brush))
graphics.FillRectangle(fillBrush, totalRect)
Dim photo As Image = Nothing
If photoCache.TryGetValue(e.SeriesPoint.Argument, photo) Then
graphics.DrawImage(photo, photoRect)
End If
End Using
e.LegendMarkerImage = image
e.DisposeLegendMarkerImage = True
Dim options As PieDrawOptions = TryCast(e.SeriesDrawOptions, PieDrawOptions)
If isSelected AndAlso options IsNot Nothing Then
options.FillStyle.FillMode = DevExpress.XtraCharts.FillMode.Hatch
CType(options.FillStyle.Options, HatchFillOptions).HatchStyle = HatchStyle.DarkDownwardDiagonal
End If
End Sub
#End Region
Private Sub OnObjectHotTracked(ByVal sender As Object, ByVal e As HotTrackEventArgs)
trackedPointArgument = If(e.HitInfo.InSeriesPoint, e.HitInfo.SeriesPoint.Argument, Nothing)
chart.Invalidate()
End Sub
Private Sub InitPhotoCache(ByVal employees As IEnumerable(Of Employee))
photoCache.Clear()
For Each employee In employees
Dim stream As New MemoryStream(employee.Photo)
If Not photoCache.ContainsKey(employee.FullName) Then
photoCache.Add(employee.FullName, Image.FromStream(stream))
End If
Next employee
End Sub
Private Function PrepareDataSource(ByVal orders As IEnumerable(Of Order)) As List(Of SalesPoint)
Dim query = From o In orders _
Group o By GroupKey = New With {Key .Year = o.OrderDate.Year, Key .Employee = o.Employee.FirstName & " " & o.Employee.LastName} Into g = Group _
Select New With {Key .Employee = GroupKey.Employee, Key .Year = GroupKey.Year, Key .Values = g.Select(Function(o)If(o.Freight.HasValue, o.Freight.Value, 0))}
Dim points As New List(Of SalesPoint)()
For Each item In query
points.Add(New SalesPoint With {.Employee = item.Employee, .Year = item.Year, .Value = item.Values.Aggregate(Function(d1, d2) d1 + d2)})
Next item
Return points
End Function
End Class
End Namespace
Friend Class SalesPoint
Public Property Employee() As String
Public Property Year() As Integer
Public Property Value() As Decimal
End Class
Imports System.ComponentModel.DataAnnotations.Schema
Imports System.ComponentModel.DataAnnotations
Imports System.Collections.Generic
Namespace CustomSeriesPointDrawingSample.Model
Partial Public Class Employee
<Key, Required, Column("EmployeeID")> _
Public Property EmployeeId() As Integer
<Required, StringLength(20)> _
Public Property LastName() As String
<Required, StringLength(10)> _
Public Property FirstName() As String
<Column(TypeName := "image")> _
Public Property Photo() As Byte()
Public ReadOnly Property FullName() As String
Get
Return FirstName & " " & LastName
End Get
End Property
Public Overridable Property Orders() As ICollection(Of Order)
End Class
End Namespace