Custom Labels or Overlay on a RadChart

8 Answers 41 Views
ChartView
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
Stephan asked on 11 Oct 2023, 04:08 PM | edited on 19 Oct 2023, 05:26 PM

Hello Telerik Team,

I want to implement some custom renderings or a overlay for a radChart. I added a screenshot of my application with some annotations. My goal is to remove the dummy resource from the scheduler. The cells of the dummy resource are only used to show workload-percentages. But before I can remove it, I need to display the workload in the ChartView, more precisely in the RangeSelector.

Screenshot

I already implemented a custom CartesianRenderer and custom BarSeriesDrawPart to control the color of the bars depending on the calculated workload. But now I want a Label with the workload percentage centered on each bar. But i don't know how to calculate the the rectange of the area from 0% to 100% to figure out the correct positioning.

My solution so far:


internal class CustomBarSeriesDrawPart : BarSeriesDrawPart
{
    public Font DefaultFont { get; private set; }


    public CustomBarSeriesDrawPart(BarSeries series, IChartRenderer renderer, Font defaultFont) : base(series, renderer)
    {
        DefaultFont = defaultFont;
    }

    public override void DrawSeriesParts()
    {
        //LogHelper.Info("CustomBarSeriesDrawPart.DrawSeriesParts() START");
        if (!(Renderer is CustomCartesianRenderer renderer))
            throw new ApplicationException($"Renderer ist nicht vom Typ \"{nameof(CustomCartesianRenderer)}\"");

        var graphics = ((ChartRenderer)Renderer).Graphics;
        var radGraphics = new RadGdiGraphics(graphics);

        for (int i = 0; i < Element.DataPoints.Count; i++)
        {
            RadRect slot = Element.DataPoints[i].LayoutSlot;

            //how to get the rectangle for the full slot (from 0% to 100% height)? 
            RectangleF barBounds = new RectangleF((float)(OffsetX + slot.X), (float)(OffsetY + slot.Y), (float)slot.Width, (float)slot.Height);

            DataPointElement childElement = (DataPointElement)Element.Children[i];

            float realWidth = barBounds.Width * childElement.HeightAspectRatio;
            barBounds.Width = realWidth;

            barBounds.Height = Math.Max(barBounds.Height, 1f);
            double percentage = 0;
            Color color;

            if (Element.DataPoints[i] is CategoricalDataPoint point)
            {
                percentage = point.Value ?? 0;

                //color = Owner.Auslastungsfarbe(percentage);
                color = renderer.Owner.Auslastungsfarbe(percentage);
            }
            else
                color = Color.LightGray;

            radGraphics.FillRectangle(barBounds, color);

            //radGraphics.DrawEllipse();

            var parameters = new TextParams()
            {
                text = $"{(int)percentage}%",
                alignment = ContentAlignment.MiddleCenter,
                foreColor = Color.Black,
                paintingRectangle = barBounds,
                font = DefaultFont,
            };
            radGraphics.DrawString(parameters, barBounds.Size);
        }
        //base.DrawSeriesParts();
    }
}

Hope you can help my with it.

Regards,

Stephan

8 Answers, 1 is accepted

Sort by
1
Accepted
Nadya | Tech Support Engineer
Telerik team
answered on 25 Oct 2023, 12:35 PM

Hello, Stephan,

In order to position the labels in RadRangeSelector on a specific place using the CustomBarLabelElementDrawPart you can manage the rect.Y value in the Draw method. I have modified it a little and now you can see the labels appear in the middle of the chart area in the range selector. I attached before/after images so you can see the differences. Feel free to adjust these values according to your specific case.

public class CustomBarLabelElementDrawPart : BarLabelElementDrawPart
{
    ChartSeries currentSeries;

    public CustomBarLabelElementDrawPart(ChartSeries series, IChartRenderer renderer) : base(series, render
    {
        currentSeries = series;
    }
    public override void Draw()
    {
        foreach (DataPointElement dataPointElement in this.Element.Children)
        {
            foreach (LabelElement label in dataPointElement.Children)
                label.ForeColor = Color.Transparent;
        }

        base.Draw();
        Graphics graphics = this.Renderer.Surface as Graphics;
        RadGdiGraphics radGraphics = new RadGdiGraphics(graphics);
        RadRect slot;
        Rectangle rect;
        StringFormat stringFormat = new StringFormat();
        stringFormat.Alignment = StringAlignment.Center;
        stringFormat.LineAlignment = StringAlignment.Center;

        foreach (DataPointElement dataPointElement in this.Element.Children)
        {
            foreach (LabelElement label in dataPointElement.Children)
            {
                label.ForeColor = Color.Transparent;
                slot = label.GetLayoutSlot();
                slot = AdjustLayoutSlot(slot, label.DataPointElement);
                rect = ChartRenderer.ToRectangle(slot);


                var viewPortHeight = ((currentSeries.Axes[1] as LinearAxis).Parent as CartesianArea).Owner.Viewport.Height;
                rect.Y = (int)viewPortHeight / 2;

                using (Brush brush = new SolidBrush(Color.Red))
                {

                    graphics.DrawString(label.Text, label.Font, brush, rect, stringFormat);
                }
            }
        }
    }
}

Before

After:

Please let me know if I can assist you further.

Regards,
Nadya
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

1
Nadya | Tech Support Engineer
Telerik team
answered on 16 Oct 2023, 09:10 AM

Hello, Stephan, 

Thank you for the provided picture.

Considering the provided information and a picture, it seems that you display some data in RadChartView and use RadRangeSelector to control which period is shown in RadScheduler. It seems that you have implement custom logic in CustomBarSeriesDrawPart. However, in order to be able to run it in my test project it is necessary to have the custom CartesianRenderer as well.

If I understand you correctly, now you would like to have a label with workload percentage shown in each bar in RadRangeSelector similar to labels in BarSeries in RadChartView. To achieve this, you can use the RangeSelectorViewElement.SeriesInitialized event where can access the ChartView via the Series. Hence, you can show labels on each bar and use the LabelMode to center them: 

chartElement.SeriesInitialized += ChartElement_SeriesInitialized;
private void ChartElement_SeriesInitialized(object sender, SeriesInitializedEventArgs e)
{
    var barSeries = e.Series as BarSeries;
    if (barSeries!=null)
    {
        barSeries.ShowLabels = true;
        barSeries.LabelMode = BarLabelModes.Center;
    }
}

In case you are having further difficulties, it would be greatly appreciated to provide more information about what exactly you are trying to achieve. Thus, we could be able to assist you further.

I hope this helps. Please let me know if I can assist you further.

Regards,
Nadya
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

1
Nadya | Tech Support Engineer
Telerik team
answered on 23 Oct 2023, 10:37 AM

Hello, Stephan,

Thank you for sharing your CustomCartesianRenderer class. 

Considering the provided picture, I suppose that you are referring to RadChartView control and would like the display labels on each bar centered on the same position (on a line in chart area) nevertheless the height of each separate bar. If I understand your requirement correctly you would like to position the labels in the middle of the chart area. In this case, if there is a bar with a lower height it would appear under the label. Please, correct me if I am missing something.

If this is your requirement, it is necessary to draw your own custom labels that would appear above the BarSeries. This can be achieved by creating a custom BarLabelElementDrawPart and overriding its Draw method. To use the custom BarLabelElementDrawPart you should use it in the CustomCartesianRenderer:

internal class CustomCartesianRenderer : CartesianRenderer
{

    public CustomCartesianRenderer(CartesianArea area)
  : base(area)
    { }
    protected override void Initialize()
    {
        base.Initialize();

        for (int i = 0; i < DrawParts.Count; i++)
        {
            if (DrawParts[i] is BarSeriesDrawPart barPart)
            {
                DrawParts[i] = new CustomBarSeriesDrawPart((BarSeries)barPart.Element, this);
            }
            if (DrawParts[i] is BarLabelElementDrawPart labelBarPart)
            {
                DrawParts[i] = new CustomBarLabelElementDrawPart((BarSeries)labelBarPart.Element, this);
            }
        }
    }

}

Here is the custom implementation for the BarLabelElementDrawPart. Note that here I position the labels in the middle of the chart area where the data points are plotted by using the height of the ViewPort element and considering the label size and padding. It is up to you to position your labels on a different Y- position according to your requirements:

public class CustomBarLabelElementDrawPart : BarLabelElementDrawPart
{
    ChartSeries currentSeries;

    public CustomBarLabelElementDrawPart(ChartSeries series, IChartRenderer renderer) : base(series, renderer)
    {
        currentSeries = series;
    }
    public override void Draw()
    {
        foreach (DataPointElement dataPointElement in this.Element.Children)
        {
            foreach (LabelElement label in dataPointElement.Children)
                label.ForeColor = Color.Transparent;
        }

        base.Draw();
        Graphics graphics = this.Renderer.Surface as Graphics;
        RadGdiGraphics radGraphics = new RadGdiGraphics(graphics);
        RadRect slot;
        Rectangle rect;
        StringFormat stringFormat = new StringFormat();
        stringFormat.Alignment = StringAlignment.Center;
        stringFormat.LineAlignment = StringAlignment.Center;

        foreach (DataPointElement dataPointElement in this.Element.Children)
        {
            foreach (LabelElement label in dataPointElement.Children)
            {
                label.ForeColor = Color.Transparent;
                slot = label.GetLayoutSlot();
                slot = AdjustLayoutSlot(slot, label.DataPointElement);
                rect = ChartRenderer.ToRectangle(slot);


                var viewPortHeight = ((currentSeries.Axes[1] as LinearAxis).Parent as CartesianArea).Owner.Viewport.Height;
                SizeF labelSize = this.MeasureLabel(label.Text, label.Font);
                rect.Y = (int)viewPortHeight / 2 + label.Padding.Top + (int)labelSize.Height + 5;

                using (Brush brush = new SolidBrush(Color.Red))
                {

                    graphics.DrawString(label.Text, label.Font, brush, rect, stringFormat);
                }
            }
        }
    }
    protected SizeF MeasureLabel(string text, Font font)
    {
        using (Graphics graphics = Graphics.FromHwnd(IntPtr.Zero))
        {
            SizeF size = Telerik.WinControls.Paint.RadGdiGraphics.MeasurementGraphics.MeasureString(text, font);
            size.Width = (float)Math.Ceiling(size.Width);
            size.Height = (float)Math.Ceiling(size.Height);

            size.Width += 2;
            size.Height += 2;
            return size;
        }
    }
}
This is the result:

I hope this helps. Let me know if I can assist you further.

Regards,
Nadya
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

0
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
answered on 19 Oct 2023, 05:15 PM | edited on 19 Oct 2023, 05:29 PM

Here is the code of my CustomCartesianRenderer:

internal class CustomCartesianRenderer : CartesianRenderer
{
    public Font DefaultFont { get; private set; }

    internal CustomControls.CalendarView Owner { get; }

    public CustomCartesianRenderer(CustomControls.CalendarView owner, CartesianArea area, Font defaultFont) : base(area)
    {
        Owner = owner ?? throw new ArgumentNullException(nameof(owner));
        DefaultFont = defaultFont;
    }


    protected override void Initialize()
    {
        base.Initialize();

        for (int i = 0; i < DrawParts.Count; i++)
        {
            if (DrawParts[i] is BarSeriesDrawPart barPart)
            {
                DrawParts[i] = new CustomBarSeriesDrawPart((BarSeries) barPart.Element, this, DefaultFont);
            }
        }
    }
}

 

I just need it to pass through the owning UserControl to get access to the color determination method, here is a simplified substitution:

///Color for workload percentage

public Color Auslastungsfarbe(double percentage) { if (percentage >= 75) return Color.Red; if (percentage >= 50) return Color.Yellow; return Color.Green; }

 

You suggested to use "BarLabelModes.Center", but this centers the label in the middle of the BarElement, but i want to center all the labels in the range between 0 to 100 like in the screen and get as close as possible to the previous look in the dummy element:


To achieve this i want to draw an overlay on top of each "bar area", the space between 0 and 100%.

0
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
answered on 23 Oct 2023, 04:49 PM | edited on 23 Oct 2023, 04:51 PM

Hello Nadya,

your help is really appreciated. I tweaked your code a bit to adjust the font and the label text to be rounded without decimal places. But unfortunately, only the ChartView is affected. The ChartView is usually not visible, it is only the datasource for the RangeSelector.

The result is what i want the RangeSelector to look like.

Screenshot

0
Nadya | Tech Support Engineer
Telerik team
answered on 24 Oct 2023, 10:16 AM

Hello, Stephan,

RadRangeSelector can also be setup to use a custom renderer just as the stand-alone RadChartView control. In order to use CustomBarLabelElementDrawPart in RadRangeSelector, you need to subscribe to the CreateRenderer event of the RangeSelectorViewElement instance. Then, use the same CustomCartesianRenderer (shown here) as in RadChartView.

Please refer to the following code snippet:

RangeSelectorViewElement chartElement = this.radRangeSelector1.RangeSelectorElement.AssociatedElement as RangeSelectorViewElement;
chartElement.CreateRenderer += ChartElement_CreateRenderer;

private void ChartElement_CreateRenderer(object sender, ChartViewCreateRendererEventArgs e)
{
    e.Renderer = new CustomCartesianRenderer((CartesianArea)e.Area);
}

I hope this will be useful.

Regards,
Nadya
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

0
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
answered on 24 Oct 2023, 02:23 PM | edited on 24 Oct 2023, 04:41 PM

Hello Nadya,

I already did that, you can see that, because the bars are differently colored depending on the value. But the labels are, somehow, positioned at the bottom. Seems that the CustomBarSeriesDrawPart is used correctly, but the CustomBarLabelElementDrawPart is not.

private void MyForm_Load() { //more stuff

if (rngTimeSelector.RangeSelectorElement.AssociatedElement is RangeSelectorViewElement chartElement) { chartElement.CreateRenderer += cvChart_CreateRenderer; chartElement.SeriesInitialized += ChartElement_SeriesInitialized; //chartElement.LabelInitializing += ChartElement_LabelInitializing; //this seems to have no effect } } private void cvChart_CreateRenderer(object sender, ChartViewCreateRendererEventArgs e) { if (e.Area is CartesianArea cartesianArea) e.Renderer = new CustomCartesianRenderer(this, cartesianArea); } private void ChartElement_SeriesInitialized(object sender, SeriesInitializedEventArgs e) { string format; switch (Timescale) { case Timescales.Hours: format = @"{0:HH:mm}"; break; case Timescales.Days: case Timescales.Weeks: format = @"{0:dd.MM.}"; break; default: thrownew ArgumentOutOfRangeException(nameof(Timescale), Timescale, $"Nicht unterstützter Wert vom Typ \"{nameof(Timescales)}\": \"{Timescale}\" ({(int)Timescale})"); } e.Series.HorizontalAxis.LabelFormat = format; if (AppointmentsVm != null) AppointmentsVm.DateLabelFormat = format; if (e.Series.VerticalAxis is LinearAxis verticalAxis) { verticalAxis.Minimum = 0; verticalAxis.Maximum = 100; } /*if (e.Series is BarSeries barSeries) { barSeries.ShowLabels = true; barSeries.LabelMode = BarLabelModes.Center; }*/ } private void ChartElement_LabelInitializing(object sender, LabelInitializingEventArgs e) { if (AppointmentsVm?.SpecialDateLabels.Contains(e.LabelElement.Text) ?? false) { //e.LabelElement.ForeColor = Color.Red; e.LabelElement.Font = new Font(e.LabelElement.Font, FontStyle.Bold); } }


internal class CustomCartesianRenderer : CartesianRenderer
{
    internal CalendarView Owner { get; }

    public CustomCartesianRenderer(CalendarView owner, CartesianArea area) : base(area)
    {
        Owner = owner ?? throw new ArgumentNullException(nameof(owner));
    }


    protected override void Initialize()
    {
        base.Initialize();

        for (int i = 0; i < DrawParts.Count; i++)
        {
            if (DrawParts[i] is BarSeriesDrawPart barPart)
            {
                DrawParts[i] = new CustomBarSeriesDrawPart((BarSeries) barPart.Element, this);
            }

            if (DrawParts[i] is BarLabelElementDrawPart labelBarPart)
            {
                DrawParts[i] = new CustomBarLabelElementDrawPart((BarSeries) labelBarPart.Element, this);
            }
        }
    }
}

Regards,

Stephan

0
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
answered on 08 Nov 2023, 05:40 PM

Hello Nadya,

thank you for your help. This solves the issue.

 

Regards,

Stephan

Tags
ChartView
Asked by
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
Answers by
Nadya | Tech Support Engineer
Telerik team
Stephan
Top achievements
Rank 3
Bronze
Iron
Iron
Share this question
or