Getting the Exact Coordinates of Data Points from a Presentation Chart in C#

image.png (76.1 KB)

The above image shows the thumbnail rendered by Aspose

image.png (36.4 KB)

This image shows is the screenshot of the same slide in PowerPoint.

I am using similar syntax as below:

chart.ValidateChartLayout()
float x = chart.ChartData.Series[0].DataPoints[0].ActualX + chart.ChartData.Series[0].DataPoints[0].ActualWidth + margin; // using the actual data point instead of zeroth in actual code
float y = chart.ChartData.Series[0].DataPoints[0].ActualY
var textbox = chart.UserShapes.Shapes.AddAutoShape(ShapeType.Rectangle, x, y, 20, 20);

The Aspose rendered image shows that the textboxes are placed correctly, but when i open the pptx in PowerPoint, I can see offsets (and these offsets are not constant values, they differ for every data point in the chart)

@apoorveslidely,
Thank you for contacting free support. Could you please share a sample presentation file so we can check the issue?

Here’s the link to the PPTx file.

This PPTx was created using Aspose (slides for .net)

@apoorveslidely,
Thank you for the sample PowerPoint presentation file. Unfortunately, I was unable to get the same result when converting the slide to an image. Please specify the following:

  • OS version on which the slide-to-image conversion was performed
  • Aspose.Slides version you used

Hey @andrey.potapov

The Aspose version I’m using is:

    <PackageReference Include="Aspose.Slides.NET" Version="25.4.0" />

The OS version is Amazon Linux 2023.7.20250428

To help you recreate the issue, here’s the exact code i used:

using System.Drawing;
using Aspose.Slides;
using Aspose.Slides.Charts;
using Aspose.Slides.Export;

namespace MyNamespace.standalone
{
    public static class StandaloneStackedBarChart
    {
        private const int DEFAULT_DATA_LABEL_FONT_SIZE = 10;

        /// <summary>
        /// Creates a PowerPoint presentation with a single slide containing a StackedBar chart with category total labels
        /// matching the specifications from design4.json
        /// </summary>
        /// <param name="filePath">Path where to save the PPTX file</param>
        public static void CreateStackedBarChartWithTotals(string filePath)
        {
            // Create presentation
            var presentation = new Presentation();
            presentation.SlideSize.SetSize(SlideSizeType.Widescreen, SlideSizeScaleType.DoNotScale);
            var slide = presentation.Slides[0];

            // Define chart position and siz
            float chartX = 40f;
            float chartY = 120f;
            float chartWidth = 550f;  
            float chartHeight = 320f;

            // Add chart to slide
            var chart = slide.Shapes.AddChart(ChartType.StackedBar, chartX, chartY, chartWidth, chartHeight);

            var categories = new List<string> 
            {
                "FCEV 400 km\n(2019)",
                "BEV 400 km\n(2019)",
                "BEV 250 km\n(2019)",
                "ICE Hybrid\n(2019)",
                "",
                "FCEV 400 km\n(Long-term)",
                "BEV 400 km\n(Long-term)",
                "ICE Hybrid\n(Long-term)"
            };

            // Series data
            var baseCarCostData = new List<double> { 21, 18, 11, 5, 0, 6, 9, 6 };
            var batteryFuelCellData = new List<double> { 6, 5, 5, 8, 0, 6, 5, 5 };
            var operationsMaintenanceData = new List<double> { 4, 2, 2, 0, 0, 2, 2, 0 };
            var electricityFuelData = new List<double> { 5, 1, 2, 1, 0, 3, 1, 0 };
            var refuelingChargingData = new List<double> { 29, 30, 30, 30, 0, 29, 29, 30 };

            // Clear existing data
            chart.ChartData.Series.Clear();
            chart.ChartData.Categories.Clear();

            // Get workbook for data management
            var workbook = chart.ChartData.ChartDataWorkbook;

            // Add categories
            for (int i = 0; i < categories.Count; i++)
            {
                chart.ChartData.Categories.Add(workbook.GetCell(0, i + 1, 0, categories[i]));
            }

            // Add series 1: Base car cost
            var chartSeries1 = chart.ChartData.Series.Add(workbook.GetCell(0, 0, 1, "Base car cost"), ChartType.StackedBar);
            chartSeries1.Format.Fill.FillType = FillType.Solid;
            chartSeries1.Format.Fill.SolidFillColor.Color = ColorTranslator.FromHtml("#BFBFBF");

            for (int i = 0; i < baseCarCostData.Count; i++)
            {
                var dataCell = workbook.GetCell(0, i + 1, 1, baseCarCostData[i]);
                chartSeries1.DataPoints.AddDataPointForBarSeries(dataCell);
            }

            // Add series 2: Battery, fuel cell
            var chartSeries2 = chart.ChartData.Series.Add(workbook.GetCell(0, 0, 2, "Battery, fuel cell"), ChartType.StackedBar);
            chartSeries2.Format.Fill.FillType = FillType.Solid;
            chartSeries2.Format.Fill.SolidFillColor.Color = ColorTranslator.FromHtml("#959595");

            for (int i = 0; i < batteryFuelCellData.Count; i++)
            {
                var dataCell = workbook.GetCell(0, i + 1, 2, batteryFuelCellData[i]);
                chartSeries2.DataPoints.AddDataPointForBarSeries(dataCell);
            }

            // Add series 3: Operations and maintenance
            var chartSeries3 = chart.ChartData.Series.Add(workbook.GetCell(0, 0, 3, "Operations and maintenance"), ChartType.StackedBar);
            chartSeries3.Format.Fill.FillType = FillType.Solid;
            chartSeries3.Format.Fill.SolidFillColor.Color = ColorTranslator.FromHtml("#6A6A6A");

            for (int i = 0; i < operationsMaintenanceData.Count; i++)
            {
                var dataCell = workbook.GetCell(0, i + 1, 3, operationsMaintenanceData[i]);
                chartSeries3.DataPoints.AddDataPointForBarSeries(dataCell);
            }

            // Add series 4: Electricity, fuel
            var chartSeries4 = chart.ChartData.Series.Add(workbook.GetCell(0, 0, 4, "Electricity, fuel"), ChartType.StackedBar);
            chartSeries4.Format.Fill.FillType = FillType.Solid;
            chartSeries4.Format.Fill.SolidFillColor.Color = ColorTranslator.FromHtml("#404040");

            for (int i = 0; i < electricityFuelData.Count; i++)
            {
                var dataCell = workbook.GetCell(0, i + 1, 4, electricityFuelData[i]);
                chartSeries4.DataPoints.AddDataPointForBarSeries(dataCell);
            }

            // Add series 5: Refueling, charging
            var chartSeries5 = chart.ChartData.Series.Add(workbook.GetCell(0, 0, 5, "Refueling, charging"), ChartType.StackedBar);
            chartSeries5.Format.Fill.FillType = FillType.Solid;
            chartSeries5.Format.Fill.SolidFillColor.Color = ColorTranslator.FromHtml("#0070C0");

            for (int i = 0; i < refuelingChargingData.Count; i++)
            {
                var dataCell = workbook.GetCell(0, i + 1, 5, refuelingChargingData[i]);
                chartSeries5.DataPoints.AddDataPointForBarSeries(dataCell);
            }

            // Set stacked overlap to 100%
            try
            {
                chartSeries1.ParentSeriesGroup.Overlap = 100;
                chartSeries2.ParentSeriesGroup.Overlap = 100;
                chartSeries3.ParentSeriesGroup.Overlap = 100;
                chartSeries4.ParentSeriesGroup.Overlap = 100;
                chartSeries5.ParentSeriesGroup.Overlap = 100;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error setting overlap: {ex.Message}");
            }

            // Configure chart appearance
            chart.HasTitle = true;
            chart.ChartTitle.AddTextFrameForOverriding("Total cost of ownership: cars (2019–long term, $ per 100 km)");
            if (chart.ChartTitle.TextFrameForOverriding?.Paragraphs?.Count > 0)
            {
                var titleFormat = chart.ChartTitle.TextFrameForOverriding.Paragraphs[0].Portions[0].PortionFormat;
                titleFormat.FontHeight = 16;
                titleFormat.FillFormat.FillType = FillType.Solid;
                titleFormat.FillFormat.SolidFillColor.Color = Color.Black;
            }
            chart.ChartTitle.Overlay = false; // Ensure title does not overlay the chart

            // Configure legend
            chart.HasLegend = true;
            chart.Legend.Position = LegendPositionType.Bottom;
            chart.Legend.TextFormat.PortionFormat.FontHeight = 9;
            
            // Configure axes
            chart.Axes.HorizontalAxis.TextFormat.PortionFormat.FontHeight = 9;
            chart.Axes.VerticalAxis.TextFormat.PortionFormat.FontHeight = 9;
            chart.Axes.VerticalAxis.HasTitle = true;
            chart.Axes.VerticalAxis.Title.AddTextFrameForOverriding("$ per 100 km");
            chart.Axes.VerticalAxis.Title.Overlay = false;

            // Add category total labels
            AddCategoryTotalLabels(chart, categories, baseCarCostData, batteryFuelCellData, operationsMaintenanceData, electricityFuelData, refuelingChargingData);

            // save an image of the slide
            using IImage image = slide.GetImage(1.0f, 1.0f);
			image.Save(filePath.Replace("pptx", "png"), ImageFormat.Png);

            // Save presentation
            presentation.Save(filePath, SaveFormat.Pptx);
            presentation.Dispose();

            Console.WriteLine($"StackedBar chart with category totals created successfully: {filePath}");
        }

        /// <summary>
        /// Adds category total labels next to each stacked bar
        /// </summary>
        private static void AddCategoryTotalLabels(IChart chart, List<string> categories, 
            List<double> series1Data, List<double> series2Data, List<double> series3Data,
            List<double> series4Data, List<double> series5Data)
        {
            try
            {
                // Validate chart layout to get actual coordinates
                chart.ValidateChartLayout();

                for (int categoryIndex = 0; categoryIndex < categories.Count; categoryIndex++)
                {
                    // Calculate the total for this category
                    double categoryTotal = series1Data[categoryIndex] + series2Data[categoryIndex] + series3Data[categoryIndex] + 
                                         series4Data[categoryIndex] + series5Data[categoryIndex];

                    // Skip empty categories
                    if (string.IsNullOrEmpty(categories[categoryIndex]) || categoryTotal == 0)
                        continue;

                    // Get the rightmost data point (last series) for positioning
                    var lastSeries = chart.ChartData.Series[chart.ChartData.Series.Count - 1];
                    if (categoryIndex < lastSeries.DataPoints.Count)
                    {
                        var lastDataPoint = lastSeries.DataPoints[categoryIndex];
                        CreateCategoryTotalLabel(chart, lastDataPoint, categoryTotal, categoryIndex);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating category total labels: {ex.Message}");
            }
        }

        /// <summary>
        /// Creates a single category total label positioned next to a bar
        /// </summary>
        private static void CreateCategoryTotalLabel(IChart chart, IChartDataPoint dataPoint, double total, int categoryIndex)
        {
            try
            {
                // Get actual layout coordinates relative to chart
                chart.ValidateChartLayout();
                var actualLayout = dataPoint.AsIActualLayout;

                // Calculate label position for bar chart (to the right of the rightmost data point)
                const float LABEL_MARGIN = 10f;
                const float LABEL_WIDTH = 20f;
                const float LABEL_HEIGHT = 20f;

                float labelX = actualLayout.ActualX + actualLayout.ActualWidth + LABEL_MARGIN; // Right of the data point
                float labelY = actualLayout.ActualY + (actualLayout.ActualHeight / 2); // Center vertically

                // Create the text label
                string labelText = FormatCategoryTotal(total);

                // Adjust position to center the textbox
                float textboxX = labelX - (LABEL_WIDTH / 2);
                float textboxY = labelY - (LABEL_HEIGHT / 2);

                // Create the textbox on the chart's UserShapes collection
                var textbox = chart.UserShapes.Shapes.AddAutoShape(ShapeType.Rectangle, textboxX, textboxY, LABEL_WIDTH, LABEL_HEIGHT);

                // Configure the textbox appearance
                textbox.FillFormat.FillType = FillType.Solid;
                textbox.FillFormat.SolidFillColor.Color = Color.Orange; // Light background
                textbox.LineFormat.FillFormat.FillType = FillType.NoFill;

                // Add the text
                textbox.TextFrame.Text = labelText;

                // Format the text
                if (textbox.TextFrame.Paragraphs.Count > 0 && textbox.TextFrame.Paragraphs[0].Portions.Count > 0)
                {
                    var portion = textbox.TextFrame.Paragraphs[0].Portions[0];
                    portion.PortionFormat.FontHeight = DEFAULT_DATA_LABEL_FONT_SIZE;
                    portion.PortionFormat.FontBold = NullableBool.True;
                    portion.PortionFormat.FillFormat.FillType = FillType.Solid;
                    portion.PortionFormat.FillFormat.SolidFillColor.Color = Color.Black;
                }

                // Center align the text
                textbox.TextFrame.Paragraphs[0].ParagraphFormat.Alignment = TextAlignment.Center;

                // Set text frame margins
                textbox.TextFrame.TextFrameFormat.MarginLeft = 2;
                textbox.TextFrame.TextFrameFormat.MarginRight = 2;
                textbox.TextFrame.TextFrameFormat.MarginTop = 2;
                textbox.TextFrame.TextFrameFormat.MarginBottom = 2;
                textbox.TextFrame.TextFrameFormat.AnchoringType = TextAnchorType.Center; // Center vertically
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating category total label for category {categoryIndex}: {ex.Message}");
            }
        }

        /// <summary>
        /// Formats the category total value for display
        /// </summary>
        private static string FormatCategoryTotal(double total)
        {
            // Format the total based on its magnitude
            if (Math.Abs(total) >= 1000000)
            {
                return $"{total / 1000000:F1}M";
            }
            else if (Math.Abs(total) >= 1000)
            {
                return $"{total / 1000:F1}K";
            }
            else if (total == (int)total)
            {
                return total.ToString("F0"); // No decimals for whole numbers
            }
            else
            {
                return total.ToString("F1"); // One decimal place
            }
        }
    }
}

Thumbnail created by Aspose:

image.png (28.6 KB)

Actual slide image from PowerPoint (just opened the created pptx in PowerPoint and took a screenshot)

image.png (13.6 KB)

@apoorveslidely,
Thank you for the additional information. I need some time to check the issue. I will get back to you as soon as possible.

@apoorveslidely,
Thank you for your patience. I have reproduced the problem with the positions of user shapes when converting the slide to an image. We apologize that you encountered this issue.

We have opened the following new ticket(s) in our internal issue tracking system and will deliver their fixes according to the terms mentioned in Free Support Policies.

Issue ID(s): SLIDESNET-45011

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.

@andrey.potapov just to make sure that we’re on the same page, the issue lies in getting the actual coordinates of a data point like chart.Series[0].DataPoints[0].ActualX

These coordinates aren’t correct (slightly offset) and cause the mismatch in actual PowerPoint.

While the image created by Aspose shows the placement of shapes (and hence the coordinates of data points) is accurate, when we open the pptx in PowerPoint, we can see that it is not.

My issue is not in converting the slide to an image, the issue is in obtaining the data point coordinates that will match the coordinates of the same data points in PowerPoint.

@apoorveslidely,
Thank you for the note. I’ve forwarded it to our developers.

@andrey.potapov any updates on this issue yet?

@apoorveslidely,
Our developers are still working on the issue. Unfortunately, I don’t have any additional information yet. Thank you for your patience.