384 lines
16 KiB
C#
384 lines
16 KiB
C#
/*******************************************************************************************
|
|
*
|
|
* raylib [shapes] example - pie chart
|
|
*
|
|
* Example complexity rating: [★★★☆] 3/4
|
|
*
|
|
* Example originally created with raylib 5.5, last time updated with raylib 5.6
|
|
*
|
|
* Example contributed by Gideon Serfontein (@GideonSerf) and reviewed by Ramon Santamaria (@raysan5)
|
|
*
|
|
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
|
|
* BSD-like license that allows static linking with closed source software
|
|
*
|
|
* Copyright (c) 2025 Gideon Serfontein (@GideonSerf)
|
|
*
|
|
********************************************************************************************/
|
|
|
|
using System;
|
|
using System.Numerics;
|
|
using System.Text;
|
|
using static Raylib_cs.Raylib;
|
|
|
|
namespace Examples.Shapes;
|
|
|
|
public partial class PieChart : IExample
|
|
{
|
|
private const int screenWidth = 800;
|
|
private const int screenHeight = 450;
|
|
|
|
private const int MAX_PIE_SLICES = 10; // Max pie slices
|
|
|
|
public string Name => "Shapes / Pie Chart";
|
|
|
|
public string Title => "raylib [shapes] example - pie chart";
|
|
|
|
private int sliceCount;
|
|
private float donutInnerRadius;
|
|
private float[] values;
|
|
private StringBuilder[] labels;
|
|
private bool[] editingLabel;
|
|
|
|
private bool showValues;
|
|
private bool showPercentages;
|
|
private bool showDonut;
|
|
private int hoveredSlice;
|
|
private Vector2 scrollContentOffset;
|
|
|
|
// UI layout parameters
|
|
private const int panelWidth = 270;
|
|
private const int panelMargin = 5;
|
|
|
|
private Vector2 panelPos;
|
|
private Rectangle panelRect;
|
|
private Rectangle canvas;
|
|
private Vector2 center;
|
|
private const float radius = 205.0f;
|
|
|
|
// Total value for percentage calculations
|
|
private float totalValue;
|
|
|
|
private int framesCounter;
|
|
|
|
// Minimal raygui-like global state
|
|
private static bool guiDisabled;
|
|
|
|
public void Init()
|
|
{
|
|
sliceCount = 7;
|
|
donutInnerRadius = 25.0f;
|
|
values = new float[MAX_PIE_SLICES] { 300.0f, 100.0f, 450.0f, 350.0f, 600.0f, 380.0f, 750.0f, 0.0f, 0.0f, 0.0f };
|
|
labels = new StringBuilder[MAX_PIE_SLICES];
|
|
editingLabel = new bool[MAX_PIE_SLICES];
|
|
|
|
for (int i = 0; i < MAX_PIE_SLICES; i++) labels[i] = new StringBuilder($"Slice {i + 1:D2}");
|
|
|
|
showValues = true;
|
|
showPercentages = false;
|
|
showDonut = false;
|
|
hoveredSlice = -1;
|
|
scrollContentOffset = new Vector2(0, 0);
|
|
|
|
// UI Panel top-left anchor
|
|
panelPos = new Vector2(
|
|
(float)screenWidth - panelMargin - panelWidth,
|
|
(float)panelMargin
|
|
);
|
|
|
|
// UI Panel rectangle
|
|
panelRect = new Rectangle(
|
|
panelPos.X, panelPos.Y,
|
|
(float)panelWidth,
|
|
(float)screenHeight - 2.0f * panelMargin
|
|
);
|
|
|
|
// Pie chart geometry
|
|
canvas = new Rectangle(0, 0, panelPos.X, (float)screenHeight);
|
|
center = new Vector2(canvas.Width / 2.0f, canvas.Height / 2.0f);
|
|
|
|
totalValue = 0.0f;
|
|
|
|
framesCounter = 0;
|
|
guiDisabled = false;
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
framesCounter++;
|
|
|
|
// Update
|
|
//----------------------------------------------------------------------------------
|
|
// Calculate total value for percentage calculations
|
|
totalValue = 0.0f;
|
|
for (int i = 0; i < sliceCount; i++) totalValue += values[i];
|
|
|
|
// Check for mouse hover over slices
|
|
hoveredSlice = -1; // Reset hovered slice
|
|
Vector2 mousePos = GetMousePosition();
|
|
if (CheckCollisionPointRec(mousePos, canvas)) // Only check if mouse is inside the canvas
|
|
{
|
|
float dx = mousePos.X - center.X;
|
|
float dy = mousePos.Y - center.Y;
|
|
float distance = MathF.Sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance <= radius) // Inside the pie radius
|
|
{
|
|
float angle = MathF.Atan2(dy, dx) * RAD2DEG;
|
|
if (angle < 0) angle += 360;
|
|
|
|
float currentAngle = 0.0f;
|
|
for (int i = 0; i < sliceCount; i++)
|
|
{
|
|
float sweep = (totalValue > 0) ? (values[i] / totalValue) * 360.0f : 0.0f;
|
|
|
|
if ((angle >= currentAngle) && (angle < (currentAngle + sweep)))
|
|
{
|
|
hoveredSlice = i;
|
|
break;
|
|
}
|
|
|
|
currentAngle += sweep;
|
|
}
|
|
}
|
|
}
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Draw
|
|
//----------------------------------------------------------------------------------
|
|
BeginDrawing();
|
|
ClearBackground(Color.RayWhite);
|
|
|
|
// Draw the pie chart on the canvas
|
|
float startAngle = 0.0f;
|
|
for (int i = 0; i < sliceCount; i++)
|
|
{
|
|
float sweepAngle = (totalValue > 0) ? (values[i] / totalValue) * 360.0f : 0.0f;
|
|
float midAngle = startAngle + sweepAngle / 2.0f; // Middle angle for label positioning
|
|
|
|
Color color = ColorFromHSV((float)i / sliceCount * 360.0f, 0.75f, 0.9f);
|
|
float currentRadius = radius;
|
|
|
|
// Make the hovered slice pop out by adding pixels to its radius
|
|
if (i == hoveredSlice) currentRadius += 20.0f;
|
|
|
|
// Draw the pie slice using raylib's DrawCircleSector function
|
|
DrawCircleSector(center, currentRadius, startAngle, startAngle + sweepAngle, 120, color);
|
|
|
|
// Draw the label for the current slice
|
|
if (values[i] > 0)
|
|
{
|
|
string labelText;
|
|
if (showValues && showPercentages) labelText = $"{values[i]:F1} ({(values[i] / totalValue) * 100.0f:F0}%)";
|
|
else if (showValues) labelText = $"{values[i]:F1}";
|
|
else if (showPercentages) labelText = $"{(values[i] / totalValue) * 100.0f:F0}%";
|
|
else labelText = "";
|
|
|
|
Vector2 textSize = MeasureTextEx(GetFontDefault(), labelText, 20, 1);
|
|
float labelRadius = radius * 0.7f;
|
|
Vector2 labelPos = new Vector2(center.X + MathF.Cos(midAngle * DEG2RAD) * labelRadius - textSize.X / 2.0f,
|
|
center.Y + MathF.Sin(midAngle * DEG2RAD) * labelRadius - textSize.Y / 2.0f);
|
|
DrawText(labelText, (int)labelPos.X, (int)labelPos.Y, 20, Color.White);
|
|
}
|
|
|
|
// Draw inner circle to create donut effect
|
|
// TODO: This is a hacky solution, better use DrawRing()
|
|
if (showDonut) DrawCircleV(center, donutInnerRadius, Color.RayWhite);
|
|
|
|
startAngle += sweepAngle;
|
|
}
|
|
|
|
// UI control panel (minimal raygui-like controls, raygui is not bound in raylib-cs)
|
|
DrawRectangleRec(panelRect, Fade(Color.LightGray, 0.5f));
|
|
DrawRectangleLinesEx(panelRect, 1.0f, Color.Gray);
|
|
|
|
GuiSpinner(new Rectangle(panelPos.X + 95, (float)panelPos.Y + 12, 125, 25), "Slices ", ref sliceCount, 1, MAX_PIE_SLICES);
|
|
GuiCheckBox(new Rectangle(panelPos.X + 20, (float)panelPos.Y + 12 + 40, 20, 20), "Show Values", ref showValues);
|
|
GuiCheckBox(new Rectangle(panelPos.X + 20, (float)panelPos.Y + 12 + 70, 20, 20), "Show Percentages", ref showPercentages);
|
|
GuiCheckBox(new Rectangle(panelPos.X + 20, (float)panelPos.Y + 12 + 100, 20, 20), "Make Donut", ref showDonut);
|
|
|
|
if (showDonut) GuiDisable();
|
|
GuiSliderBar(new Rectangle(panelPos.X + 80, (float)panelPos.Y + 12 + 130, panelRect.Width - 100, 30),
|
|
"Inner Radius", null, ref donutInnerRadius, 5.0f, radius - 10.0f);
|
|
GuiEnable();
|
|
|
|
GuiLine(new Rectangle(panelPos.X + 10, (float)panelPos.Y + 12 + 170, panelRect.Width - 20, 1));
|
|
|
|
// Scrollable area for slice editors
|
|
float scrollTop = (float)panelPos.Y + 12 + 190;
|
|
Rectangle scrollPanelBounds = new Rectangle(
|
|
panelPos.X + panelMargin,
|
|
scrollTop,
|
|
panelRect.Width - panelMargin * 2,
|
|
(panelRect.Y + panelRect.Height) - scrollTop - panelMargin);
|
|
int contentHeight = sliceCount * 35;
|
|
|
|
// Simple vertical scroll via mouse wheel while hovering the panel
|
|
if (CheckCollisionPointRec(GetMousePosition(), scrollPanelBounds))
|
|
{
|
|
scrollContentOffset.Y += GetMouseWheelMove() * 20.0f;
|
|
float minOffset = MathF.Min(0.0f, scrollPanelBounds.Height - contentHeight);
|
|
if (scrollContentOffset.Y < minOffset) scrollContentOffset.Y = minOffset;
|
|
if (scrollContentOffset.Y > 0.0f) scrollContentOffset.Y = 0.0f;
|
|
}
|
|
|
|
Rectangle view = scrollPanelBounds;
|
|
float contentX = view.X + scrollContentOffset.X; // Left of content
|
|
float contentY = view.Y + scrollContentOffset.Y; // Top of content
|
|
|
|
BeginScissorMode((int)view.X, (int)view.Y, (int)view.Width, (int)view.Height);
|
|
|
|
for (int i = 0; i < sliceCount; i++)
|
|
{
|
|
int rowY = (int)(contentY + 5 + i * 35);
|
|
|
|
// Color indicator
|
|
Color color = ColorFromHSV((float)i / sliceCount * 360.0f, 0.75f, 0.9f);
|
|
DrawRectangle((int)(contentX + 15), rowY + 5, 20, 20, color);
|
|
|
|
// Label textbox
|
|
if (GuiTextBox(new Rectangle(contentX + 45, (float)rowY, 75, 30), labels[i], 32, editingLabel[i])) editingLabel[i] = !editingLabel[i];
|
|
|
|
GuiSliderBar(new Rectangle(contentX + 130, (float)rowY, 110, 30), null, null, ref values[i], 0.0f, 1000.0f);
|
|
}
|
|
|
|
EndScissorMode();
|
|
|
|
EndDrawing();
|
|
//----------------------------------------------------------------------------------
|
|
}
|
|
|
|
public void Unload()
|
|
{
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Minimal raygui-like widgets (plain raylib re-implementation)
|
|
//----------------------------------------------------------------------------------
|
|
private static void GuiDisable() => guiDisabled = true;
|
|
private static void GuiEnable() => guiDisabled = false;
|
|
|
|
private static void GuiLine(Rectangle bounds)
|
|
{
|
|
int y = (int)(bounds.Y + bounds.Height / 2);
|
|
DrawLine((int)bounds.X, y, (int)(bounds.X + bounds.Width), y, Color.Gray);
|
|
}
|
|
|
|
private static void GuiCheckBox(Rectangle bounds, string text, ref bool active)
|
|
{
|
|
Vector2 mouse = GetMousePosition();
|
|
bool hover = CheckCollisionPointRec(mouse, bounds);
|
|
if (!guiDisabled && hover && IsMouseButtonPressed(MouseButton.Left)) active = !active;
|
|
|
|
DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray);
|
|
if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray);
|
|
if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
|
|
}
|
|
|
|
private static void GuiSpinner(Rectangle bounds, string text, ref int value, int minValue, int maxValue)
|
|
{
|
|
Vector2 mouse = GetMousePosition();
|
|
Rectangle left = new Rectangle(bounds.X, bounds.Y, bounds.Height, bounds.Height);
|
|
Rectangle right = new Rectangle(bounds.X + bounds.Width - bounds.Height, bounds.Y, bounds.Height, bounds.Height);
|
|
Rectangle mid = new Rectangle(bounds.X + bounds.Height, bounds.Y, bounds.Width - 2 * bounds.Height, bounds.Height);
|
|
|
|
if (!guiDisabled && CheckCollisionPointRec(mouse, left) && IsMouseButtonPressed(MouseButton.Left) && value > minValue) value--;
|
|
if (!guiDisabled && CheckCollisionPointRec(mouse, right) && IsMouseButtonPressed(MouseButton.Left) && value < maxValue) value++;
|
|
|
|
DrawRectangleRec(mid, Color.RayWhite);
|
|
DrawRectangleLinesEx(mid, 1, Color.Gray);
|
|
DrawRectangleRec(left, Color.LightGray);
|
|
DrawRectangleLinesEx(left, 1, Color.Gray);
|
|
DrawRectangleRec(right, Color.LightGray);
|
|
DrawRectangleLinesEx(right, 1, Color.Gray);
|
|
DrawText("-", (int)(left.X + left.Width / 2 - 2), (int)(left.Y + left.Height / 2 - 5), 10, Color.DarkGray);
|
|
DrawText("+", (int)(right.X + right.Width / 2 - 3), (int)(right.Y + right.Height / 2 - 5), 10, Color.DarkGray);
|
|
string vs = value.ToString();
|
|
DrawText(vs, (int)(mid.X + mid.Width / 2 - MeasureText(vs, 10) / 2), (int)(mid.Y + mid.Height / 2 - 5), 10, Color.DarkGray);
|
|
if (text != null) DrawText(text, (int)bounds.X - MeasureText(text, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
|
|
}
|
|
|
|
private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue)
|
|
{
|
|
Vector2 mouse = GetMousePosition();
|
|
bool hover = CheckCollisionPointRec(mouse, bounds);
|
|
if (!guiDisabled && hover && IsMouseButtonDown(MouseButton.Left))
|
|
{
|
|
value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue);
|
|
if (value < minValue) value = minValue;
|
|
if (value > maxValue) value = maxValue;
|
|
}
|
|
|
|
DrawRectangleRec(bounds, guiDisabled ? Color.Gray : Color.LightGray);
|
|
float pct = (value - minValue) / (maxValue - minValue);
|
|
DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), guiDisabled ? Color.DarkGray : Color.SkyBlue);
|
|
DrawRectangleLinesEx(bounds, 1, Color.Gray);
|
|
if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
|
|
if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
|
|
}
|
|
|
|
private bool GuiTextBox(Rectangle bounds, StringBuilder text, int maxChars, bool editMode)
|
|
{
|
|
bool pressed = false;
|
|
Vector2 mouse = GetMousePosition();
|
|
bool hover = CheckCollisionPointRec(mouse, bounds);
|
|
|
|
if (editMode)
|
|
{
|
|
int key = GetCharPressed();
|
|
while (key > 0)
|
|
{
|
|
if ((key >= 32) && (key <= 125) && (text.Length < maxChars - 1))
|
|
{
|
|
text.Append((char)key);
|
|
}
|
|
|
|
key = GetCharPressed();
|
|
}
|
|
|
|
if (IsKeyPressed(KeyboardKey.Backspace) && (text.Length > 0)) text.Remove(text.Length - 1, 1);
|
|
}
|
|
|
|
DrawRectangleRec(bounds, Color.RayWhite);
|
|
DrawRectangleLinesEx(bounds, editMode ? 2 : 1, editMode ? Color.Red : (hover ? Color.Blue : Color.Gray));
|
|
|
|
string content = text.ToString();
|
|
DrawText(content, (int)bounds.X + 4, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
|
|
|
|
if (editMode && ((framesCounter / 20) % 2 == 0))
|
|
{
|
|
DrawText("_", (int)bounds.X + 4 + MeasureText(content, 10), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
|
|
}
|
|
|
|
if (hover && IsMouseButtonPressed(MouseButton.Left)) pressed = true;
|
|
|
|
return pressed;
|
|
}
|
|
|
|
public static int Main()
|
|
{
|
|
// Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
InitWindow(screenWidth, screenHeight, "raylib [shapes] example - pie chart");
|
|
|
|
SetTargetFPS(60);
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
var game = new PieChart();
|
|
game.Init();
|
|
|
|
// Main game loop
|
|
while (!WindowShouldClose())
|
|
{
|
|
game.Update();
|
|
}
|
|
|
|
game.Unload();
|
|
|
|
// De-Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
CloseWindow(); // Close window and OpenGL context
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
return 0;
|
|
}
|
|
}
|