首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP GD 繪製 3D 立體餅圖:從數學原理到完整類實現

     編輯:PHP 相關知識

3D 餅圖的幾何本質

3D 餅圖本質是**橢圓柱體切片的正投影**。每個扇區對應一個「楔形柱體」,由以下要素構成:

  • 頂面:橢圓扇形(繪製數據比例)
  • 側面:矩形側面(體現 3D 厚度)
  • 邊緣/陰影:增強立體感

核心數學計算

// 橢圓參數方程:x = a·cosθ, y = b·sinθ
// 扇形角度:$angle = ($value / $total) * 360
// 3D 側面:將頂面沿 Y 軸下移 $depth 像素

完整 3D 餅圖類實現

<?php
class PieChart3D {
    private int $width;
    private int $height;
    private int $centerX;
    private int $centerY;
    private int $radiusX;      // 橢圓長半軸
    private int $radiusY;      // 橢圓短半軸
    private int $depth = 20;   // 3D 厚度
    private array $data = [];
    private array $colors = [];
    private array $labels = [];
    private resource $image;
    private int $total = 0;

    public function __construct(int $width = 600, int $height = 400) {
        $this->width = $width;
        $this->height = $height;
        $this->centerX = $width / 2;
        $this->centerY = $height / 2;
        $this->radiusX = min($width, $height) / 2 - 40;
        $this->radiusY = (int)($this->radiusX * 0.5); // 橢圓扁平比例 0.5
    }

    public function setData(array $values, array $labels = [], array $colors = []): self {
        $this->data = array_values($values);
        $this->labels = $labels ?: array_keys($values);
        $this->total = array_sum($this->data);
        
        // 默認配色方案
        $defaultColors = [
            0xFF6B6B, 0x4ECDC4, 0xFFE66D, 0x95E1D3,
            0xF38181, 0xAA96DA, 0xFCBAD3, 0xA8D8EA
        ];
        $this->colors = $colors ?: array_slice($defaultColors, 0, count($this->data));
        return $this;
    }

    public function setDepth(int $depth): self {
        $this->depth = max(5, min(50, $depth));
        return $this;
    }

    public function render(string $format = 'png'): void {
        $this->createCanvas();
        $this->drawShadowBase();      // 1. 繪製底部陰影
        $this->drawSideFaces();       // 2. 繪製側面(從後往前)
        $this->drawTopFaces();        // 3. 繪製頂面
        $this->drawLabels();          // 4. 繪製標籤
        $this->output($format);
    }

    private function createCanvas(): void {
        $this->image = imagecreatetruecolor($this->width, $this->height);
        // 透明背景
        $transparent = imagecolorallocatealpha($this->image, 255, 255, 255, 127);
        imagefill($this->image, 0, 0, $transparent);
        imagesavealpha($this->image, true);
    }

    private function hexToRgb(int $hex): array {
        return [
            ($hex >> 16) & 0xFF,
            ($hex >> 8) & 0xFF,
            $hex & 0xFF
        ];
    }

    private function allocateColor(int $hex, int $alpha = 0): int {
        [$r, $g, $b] = $this->hexToRgb($hex);
        return imagecolorallocatealpha($this->image, $r, $g, $b, $alpha);
    }

    private function darkerColor(int $hex, int $factor = 40): int {
        [$r, $g, $b] = $this->hexToRgb($hex);
        return $this->allocateColor(
            (max(0, $r - $factor) << 16) | 
            (max(0, $g - $factor) << 8) | 
            max(0, $b - $factor)
        );
    }

    // 橢圓上點的坐標
    private function ellipsePoint(float $deg): array {
        $rad = deg2rad($deg);
        return [
            $this->centerX + $this->radiusX * cos($rad),
            $this->centerY + $this->radiusY * sin($rad)
        ];
    }

    // 繪製底部陰影(整個餅圖的投影)
    private function drawShadowBase(): void {
        $shadowColor = imagecolorallocatealpha($this->image, 0, 0, 0, 80);
        for ($i = 1; $i <= $this->depth; $i++) {
            imagefilledellipse(
                $this->image,
                $this->centerX,
                $this->centerY + $i,
                $this->radiusX * 2,
                $this->radiusY * 2,
                $shadowColor
            );
        }
    }

    // 繪製側面(關鍵:按角度從大到小排序,避免遮擋錯誤)
    private function drawSideFaces(): void {
        $sectors = [];
        $startDeg = 0;
        foreach ($this->data as $i => $value) {
            $angle = ($value / $this->total) * 360;
            $sectors[] = [
                'index' => $i,
                'start' => $startDeg,
                'end' => $startDeg + $angle,
                'mid' => $startDeg + $angle / 2
            ];
            $startDeg += $angle;
        }
        // 後面的先畫:按中點角度排序(180° 附近最前)
        usort($sectors, fn($a, $b) => 
            abs($b['mid'] - 180) <=> abs($a['mid'] - 180)
        );

        foreach ($sectors as $sector) {
            $this->drawSingleSide($sector);
        }
    }

    private function drawSingleSide(array $sector): void {
        $i = $sector['index'];
        $start = $sector['start'];
        $end = $sector['end'];
        $color = $this->allocateColor($this->colors[$i]);
        $darkColor = $this->darkerColor($this->colors[$i], 50);

        // 側面由多邊形構成:頂面弧線 -> 下移 -> 底面弧線 -> 回頂面
        $points = [];
        // 頂面弧線取樣點
        for ($deg = $start; $deg <= $end; $deg += 2) {
            [$x, $y] = $this->ellipsePoint($deg);
            $points[] = $x;
            $points[] = $y;
        }
        // 底面弧線取樣點(反向)
        for ($deg = $end; $deg >= $start; $deg -= 2) {
            [$x, $y] = $this->ellipsePoint($deg);
            $points[] = $x;
            $points[] = $y + $this->depth;
        }

        if (count($points) >= 6) {
            imagefilledpolygon($this->image, $points, count($points)/2, $darkColor);
        }
        
        // 繪製兩側邊緣線
        [$x1, $y1] = $this->ellipsePoint($start);
        [$x2, $y2] = $this->ellipsePoint($end);
        imageline($this->image, $x1, $y1, $x1, $y1 + $this->depth, $darkColor);
        imageline($this->image, $x2, $y2, $x2, $y2 + $this->depth, $darkColor);
    }

    // 繪製頂面扇形
    private function drawTopFaces(): void {
        $startDeg = 0;
        foreach ($this->data as $i => $value) {
            $angle = ($value / $this->total) * 360;
            $endDeg = $startDeg + $angle;
            $color = $this->allocateColor($this->colors[$i]);

            // 使用 imagefilledarc 繪製頂面(GD 內置支持扇形填充)
            imagefilledarc(
                $this->image,
                $this->centerX,
                $this->centerY,
                $this->radiusX * 2,
                $this->radiusY * 2,
                $startDeg,
                $endDeg,
                $color,
                IMG_ARC_PIE | IMG_ARC_NOFILL // 只要邊緣?不,要填充
            );
            // 修正:IMG_ARC_PIE 已包含填充
            imagefilledarc(
                $this->image,
                $this->centerX,
                $this->centerY,
                $this->radiusX * 2,
                $this->radiusY * 2,
                $startDeg,
                $endDeg,
                $color,
                IMG_ARC_PIE
            );

            $startDeg = $endDeg;
        }
    }

    // 繪製標籤與百分比
    private function drawLabels(): void {
        $startDeg = 0;
        $font = __DIR__ . '/fonts/DejaVuSans.ttf'; // 需自備 TTF 字體
        $fontSize = 12;
        $labelColor = $this->allocateColor(0x333333);

        foreach ($this->data as $i => $value) {
            $angle = ($value / $this->total) * 360;
            $midDeg = $startDeg + $angle / 2;
            $percent = ($value / $this->total) * 100;
            
            // 標籤位置:稍微超出橢圓外緣
            $labelRadiusX = $this->radiusX * 1.2;
            $labelRadiusY = $this->radiusY * 1.2;
            $rad = deg2rad($midDeg);
            $lx = $this->centerX + $labelRadiusX * cos($rad);
            $ly = $this->centerY + $labelRadiusY * sin($rad);

            $label = ($this->labels[$i] ?? '') . " " . number_format($percent, 1) . "%";
            
            // 計算文本寬高居中
            $box = imagettfbbox($fontSize, 0, $font, $label);
            $tw = $box[2] - $box[0];
            $th = $box[1] - $box[7];
            
            imagettftext(
                $this->image, $fontSize, 0,
                $lx - $tw/2, $ly + $th/2,
                $labelColor, $font, $label
            );

            $startDeg += $angle;
        }
    }

    private function output(string $format): void {
        header('Content-Type: image/' . ($format === 'jpg' ? 'jpeg' : $format));
        match ($format) {
            'png' => imagepng($this->image),
            'jpg', 'jpeg' => imagejpeg($this->image, null, 90),
            'gif' => imagegif($this->image),
            default => imagepng($this->image)
        };
        imagedestroy($this->image);
    }
}

使用示例

<?php
require 'PieChart3D.php';

$data = [
    '搜索引擎' => 4500,
    '直接訪問' => 3200,
    '外部鏈接' => 1800,
    '社交媒體' => 1200,
    '廣告推廣' => 800,
    '其他' => 500
];

$chart = new PieChart3D(800, 500);
$chart->setData(
    array_values($data),
    array_keys($data),
    [0xE74C3C, 0x3498DB, 0x2ECC71, 0xF39C12, 0x9B59B6, 0x95A5A6]
)
->setDepth(25)
->render('png');

進階優化方向

  1. 抗鋸齒:使用 imageantialias($img, true) 或超采樣(2x 畫布再縮放)
  2. 漸變填充:側面和頂面應用線性漸變模擬光照
  3. 圖例組件:獨立繪製圖例區域,支持點擊篩選(需前端配合)
  4. 動畫生成:輸出幀序列合成 GIF/APNG
  5. 性能優化:大數據量時預計算路徑、緩存像素點

替代方案對比

方案優點缺點適用場景
GD 手繪零依賴、完全可控、無授權問題代碼複雜、抗鋸齒弱、無交互簡單報表、郵件嵌入、受限環境
pChart功能豐富、支持漸變/陰影/圖例維護停滯、PHP 8 兼容性差老項目維護
Chart.js (前端)交互豐富、動畫流暢、響應式需 JS 環境、無法直接生成圖片儀表板、用戶可見頁面
QuickChart APIURL 生成圖表、支持 Chart.js 配置外部依賴、頻率限制快速集成、Serverless
Python Matplotlib專業級、出版質量、3D 支持強需跨語言調用、部署複雜數據科學報表、科研論文

總結

GD 繪製 3D 餅圖的核心在於**繪製順序控制**(陰影→側面→頂面→標籤)和**橢圓幾何計算**。雖然代碼量較大,但零依賴、可完全定制,適合服務端生成靜態圖表場景。現代 Web 應用建議優先考慮前端圖表庫,服務端僅提供數據 API。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved