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)