65.9K
CodeProject 正在变化。 阅读更多。
Home

纯 PHP 中的折线图

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2020年9月16日

CPOL

9分钟阅读

viewsIcon

19185

downloadIcon

528

一个可以添加到你的 PHP 页面以创建折线图的类的说明

引言

要在网页上使用 PHP 添加图表,您可以使用像 pChart 这样的工具,但如果您只需要一个简单的折线图,那可能有点过度了。而且,自己动手也很有趣。所以,让我们用纯 PHP 构建一个折线图。您可以在自己的代码中使用 Chart 类,并根据自己的喜好修改 ChartChartDraw 类;例如,您可能更喜欢柱状图。

折线图包括

  • 一个水平 x 轴和一个垂直 y 轴;惯例是将图表上的位置记为 (x, y)。左下角是 (0, 0) 位置。
  • 通常有一个水平的上边线和垂直的右边线来构成一个矩形
  • 通常在 y 轴左侧和 x 轴下方有一些标签
  • 以及连接数据点的所有线条。

我将尝试一致地将所有这些称为“图表”,如果我只指连接数据点的线条,则使用“线条”。

我使用的是 PHP 7.1.22 和 PHP 内置的 Web 服务器 php.exe 在 Windows 10 PC 上,但该代码应该可以在其他环境的 PHP 5 上运行。我从 imagesetpixel 的示例和一个 来自 stackoverflow 的提示 开始,用于 show() 函数。使用的 PHP 图像函数有 imagecreatetruecolor()imagecolorallocate()imagesetpixel() 用于线条,以及 imagestring()imagefontheight()imagefontwidth() 用于标签。

代码

代码由三个 PHP 文件组成

  • testChart.php 作为使用 Chart 类的示例
  • chart.php 包含 Chart 类,该类使用
  • chartDraw.php 包含不带标签的 ChartDraw

要运行代码,将这三个文件保存在同一个目录中,然后在浏览器中加载 testChart.php

Using the Code

如果您想将折线图添加到您自己的项目中,请将 testChart.php 替换为您自己的代码。在 testChart 代码中,创建了三条线,蓝色、红色和绿色。数据元素是

    // y1 for the blue line, y2 for the red line
	// xlabel is not used in this example, see below 
	$resultArray[0]  = array('x'=>0, 'xlabel'=>'DEC', 'y1'=>2000, 'y2'=>1600); // x = days 
                                                                               // since 01.01
	$resultArray[1]  = array('x'=>31, 'xlabel'=>'JAN', 'y1'=>1800, 'y2'=>1700);
	$resultArray[2]  = array('x'=>59, 'xlabel'=>'FEB', 'y1'=>1700, 'y2'=>1800);
	$resultArray[3]  = array('x'=>90, 'xlabel'=>'MAR', 'y1'=>1400, 'y2'=>1600);
	$resultArray[4]  = array('x'=>120, 'xlabel'=>'APR', 'y1'=>1250, 'y2'=>1400);
	$resultArray[5]  = array('x'=>151, 'xlabel'=>'MAY', 'y1'=>1900, 'y2'=>1300);
	$resultArray[6]  = array('x'=>181, 'xlabel'=>'JUN', 'y1'=>2050, 'y2'=>1500);
	$resultArray[7]  = array('x'=>212, 'xlabel'=>'JUL', 'y1'=>2200, 'y2'=>1700);
	$resultArray[8]  = array('x'=>243, 'xlabel'=>'AUG', 'y1'=>2100, 'y2'=>1900);
	$resultArray[9]  = array('x'=>273, 'xlabel'=>'SEP', 'y1'=>2300, 'y2'=>1800);
	$resultArray[10] = array('x'=>304, 'xlabel'=>'OCT', 'y1'=>2350, 'y2'=>2000);
	$resultArray[11] = array('x'=>334, 'xlabel'=>'NOV', 'y1'=>2400, 'y2'=>2100);
	$resultArray[12] = array('x'=>365, 'xlabel'=>'DEC', 'y1'=>2450, 'y2'=>2200);
	
	// for the green line
	$resultGreenArray[0] = 1500;
	$resultGreenArray[1] = 1900;
	$resultGreenArray[2] = 1800;
	$resultGreenArray[3] = 1500;
	$resultGreenArray[4] = 1400;

包含测试数据的数组 resultArray,其中 'y1' 代表蓝色线,'y2' 代表红色线。对于 x,我们可以直接使用数组的索引:我添加了 'x' 列来显示 x 值不必等距。 resultGreenArray 显示了最基本的形式,数组索引就是 x 轴元素。

构建线条并显示图表的代码是

	$chart = new Chart();
	$chart->setPixelSize(600, 400, 2);
	$chart->setMinMaxX(0, 365, 3);					
	$chart->setMinMaxY(500, 3000);
	
	// blue line
	$errorMessage = $chart->addNewLine(0, 0, 255); // blue
	foreach ($resultArray as $i=>$valueArray) {
		$errorMessage = $chart->setPoint($valueArray['x'], $valueArray['y1'], 
                        strval($valueArray['x']));
// if you prefer the xlabel text values, make the previous line comment 
// and uncomment the next line
//		$chart->setPoint($valueArray['x'], $valueArray['y1'], $valueArray['xlabel']); 
	}
		
	// red line
	$errorMessage = $chart->addNewLine(255, 0, 0); // red
	foreach ($resultArray as $i=>$valueArray) {
		$errorMessage = $chart->setPoint($valueArray['x'], $valueArray['y2'], '');
	}
		
	// green line
	$errorMessage = $chart->addNewLine(0, 255, 0); // green
	$chart->setMinMaxX(0, 4, 0);					
	foreach ($resultGreenArray as $i=>$value) {
		$errorMessage = $chart->setPoint($i, $value, '');
	}

	// show
	
	$chart->show(5);

前几行创建图表并指定尺寸,包括像素尺寸和数据尺寸(下文解释)。setPixelSize() 的第三个参数是 字体,内置字体的值在 1 到 5 之间。setMinMaxX() 的第三个参数是 x 轴上最右边标签的长度,我们在 ChartDraw 中需要它来设置边距。

	$chart = new Chart();
	$chart->setPixelSize(600, 400, 2);
	$chart->setMinMaxX(0, 365, 3);					
	$chart->setMinMaxy(500, 3000);

接下来,创建三条线。setPoint() 的第三个参数指定 x 轴上的标签。对于测试数据,这对于第一条线是有意义的,但对于第二条线和第三条线则不然。如果一切正常,$errorMessage 是一个空字符串,在这个例子中,我省略了出现错误时应有的代码。

最后,show() 显示图表。参数指定 y 轴左侧的标签数量。

注释

  • setPoint() 必须按 x 值递增的顺序调用
  • x 可以从非 0 值开始,但不能为负值
  • x 标签可以是字符串文本
  • y 值和 minY 可能是负数
  • y 标签是(数值上等于)y
  • 如果您的数据很接近,您可能希望只为大约每五个点设置一个 x 标签,以确保 x 标签不重叠。一种方法是添加一个带有您想要的标签的白色线条来设置 x 标签。
  • minXmaxXminYmaxY 可以由 Chart 类自身计算。TestChart.php 表明这并不总是您想要的 x 值。在 Chart 中确定 minYmaxY 需要将所有线条的数据保存在 Chart 中,然后计算 minYmaxY,然后才开始绘制线条。这可以做到,但会使本文的代码不那么清晰。

图表步骤

图表由一个像素矩形组成,在 TestChart.php 中,宽度为 600 像素,高度为 400 像素。这些是图表的物理尺寸,ChartDraw 类只知道这些。但是,我们的数据并不适合这些尺寸:12 个数据元素必须分配到 600 像素上,并且值必须减小以适应 400 像素的高度。这在 Chart 类中完成。

Chart.php 中的前三个函数设置图表的像素尺寸以及 xyminmax 值(我以小写的 a 开始函数参数的名称;没有微妙的技术原因,只是为了在代码中提醒它是一个参数)。我们还需要 x 轴标签的最大字符串长度,以确定边距。

	public function setPixelSize($aWidth, $aHeight, $aFontSize) 
	{
		$this->width = $aWidth;
		$this->height = $aHeight;
		$this->fontSize = $aFontSize;
	}

	public function setMinMaxX($aMinX, $aMaxX, $aRightTextLengthX) 
	{
		$this->minX = $aMinX;
		$this->maxX = $aMaxX;
		$this->rightTextLengthX = $aRightTextLengthX;
	}
	
	public function setMinMaxY($aMinY, $aMaxY) 
	{
		$this->minY = $aMinY;
		$this->maxY = $aMaxY;
		// if $aMinY negative, the text length can be longer than $aMaxY
		$this->maxTextLengthY = max(strlen(strval($aMinY)), strlen(strval($aMaxY)));
	}

对于您想要绘制的每一条线,您都必须调用 addNewLine()

	public function addNewLine($aRed, $aGreen, $aBlue) 
	{
		if ($this->chartDraw == null) { // create at first call of this function
			$errorMessage = $this->validateParameters();
			if ($errorMessage != '') {
				return $errorMessage;
			}
			$this->chartDraw = new ChartDraw($this->width, $this->height, $this->fontSize
				, $this->maxTextLengthY, $this->maxTextLengthY);
		}
		
		$this->chartDraw->addNewLine($aRed, $aGreen, $aBlue);
		return '';
	}

第一次调用此函数时,将初始化 chartDraw 类(带有我们稍后将讨论的许多参数)。之所以在此处进行,是因为所有参数都必须已设置。请注意,此函数和下一个函数会返回一个 errorMessage,如果没有找到错误,则为空 string

接下来,setPoint() 是此类中的关键函数。

	
	public function setPoint($aX, $aY, $aXLabelText) 
	{
		$errorMessage = $this->validateXY($aX, $aY);
		if ($errorMessage != '') {
			return $errorMessage;
		}
		
		$xPixel = round(($aX - $this->minX) * $this->width / ($this->maxX - $this->minX));
		$yPixel = round(($aY - $this->minY) * $this->height / ($this->maxY - $this->minY));

		$this->chartDraw->set($xPixel, $yPixel, $aXLabelText);
		return '';
	}

此函数为每个数据元素调用。请注意,必须按 $aX 递增的顺序调用该函数(否则 ChartDraw 类中的 connectTwoPoints() 将无法正常工作)。$xPixel$yPixel 行将“像素”尺寸从“数据”尺寸计算出来:如果 $aX = $this->minX,则 $xPixel 计算为 0。如果 $aX = $this->maxX,则 $xPixel 计算为 $this->width,因此所有 $aX 值都将适合图表的“像素边界”。

最后,我们将点的位置传递给 chartDraw 中的 set(),包括 x 轴下方的标签文本。

Chart 类中的其他 public 函数是

  • show():您代码中的最终函数调用,用于设置 y 轴左侧的标签并显示图表。作为 show() 的参数,您给出 y 轴上想要的标签数量。

private 函数是

  • setYLabels():设置 y 轴左侧的标签
  • validateParameters():如果必需参数未设置,则返回消息
  • validateXY():当 x 或 y 超出范围时返回消息

ChartDraw 步骤

Chart 类中,我们将数据元素作为 (x, y) 对传递(已转换为像素尺寸)。这假定 (0, 0) 是下一个图中白色矩形的原点。

但我们也需要蓝色部分

  • x 轴下方的标签空间
  • 右侧的一小块空间,因为我们以 x 值为中心对齐标签,所以最右边的标签会稍微超出“白色”矩形的最右边。
  • y 轴左侧的标签空间
  • 顶部的一小块空间,因为我们以 y 值为中心对齐标签,所以最顶部的标签会稍微超出“白色”矩形的顶部。

另一个挑战是,PHP 函数 imagesetpixel()imagestring() 将左上角视为原点 (0, 0),而图表的通常做法是将左下角视为原点。

ChartDraw 类的任务是处理所有这些并将其隐藏在 Chart 类之外。要在 ChartDraw 中设置像素,我们使用

	private function setPixel($aX, $aY, $aColor) 
	{
		imagesetpixel($this->gd, $this->marginLeftX + $aX, 
                      $this->sizeY + $this->marginTopY - $aY, $aColor);
	}    

所以对于 setPixel(),点 (0, 0) 是白色矩形的左下角。

ChartDraw 类的构造函数中,我们将参数 $aSizeX$aSizeY$aFontSize 保存以备后用。请注意,我们现在切换到 X 和 Y,这是图表的命名约定,而在 Chart 类中,我们使用 widthheight 来表示像素尺寸。从 $aRightTextLengthX$aMaxTextLengthY,我们在 setMargins() 中计算边距,然后使用 imagecreatetruecolor() 创建图表(包括“蓝色”部分)的矩形。+1 的原因是:如果低值是 0,高值是 n,我们有 n 个区间但 n + 1 个点。

如果您不做其他任何事情并显示结果,它将是一个黑色矩形。因此,我们定义白色,然后可以将所有像素设置为白色。最后,我们绘制一个黑色边框(围绕“白色”矩形)。

	public function __construct($aSizeX, $aSizeY, $aFontSize, 
                                $aRightTextLengthX, $aMaxTextLengthY) 
	{
		$this->sizeX = $aSizeX;
		$this->sizeY = $aSizeY;
		$this->fontSize = $aFontSize;
		
		$this->setMargins($aRightTextLengthX, $aMaxTextLengthY);

		$this->gd = imagecreatetruecolor($this->sizeX + 1 + 
                    $this->marginLeftX + $this->marginRightX
			, $this->sizeY + 1 + $this->marginBottomY + $this->marginTopY);
 
		// this creates a black rectangle, so make everything white:
 
		$white = imagecolorallocate($this->gd, 255, 255, 255);	

		for ($x = 0; $x <= $this->sizeX + $this->marginLeftX + $this->marginRightX; $x++) {
			for ($y = 0; $y <= $this->sizeY + $this->marginBottomY + $this->marginTopY; $y++) {
				imagesetpixel($this->gd, $x, $y, $white);
			}
		}
		
		// set border lines

		$this->black = imagecolorallocate($this->gd, 0, 0, 0); // also need this one later

		for ($x = 0; $x <= $this->sizeX; $x++) {
			$this->setPixel($x, 0, $this->black);              // bottom x-axis
			$this->setPixel($x, $this->sizeY, $this->black);   // top x-line
		}
		for ($y = 0; $y <= $this->sizeY; $y++) {
			$this->setPixel(0, $y, $this->black);              // left y-axis
			$this->setPixel($this->sizeX, $y, $this->black);   // right y-line
		}
	}

当开始一条新线时,addNewLine()Chart 类调用。

	public function addNewLine($aRed, $aGreen, $aBlue) 
	{
		$this->lineColor = imagecolorallocate($this->gd, $aRed, $aGreen, $aBlue);

		$this->previousX = -1;
	}    

需要此函数来设置线的颜色,同时也重置 $previous,以便我们在 set() 中获取第一个点时知道。

关键函数是 set()

	public function set($aX, $aY, $aText) 
	{	
		$this->setPixel($aX, $aY, $this->lineColor);
		$this->setLabelX($aX, $aText);

		if ($this->previousX != -1) {
			$this->connectTwoPoints($this->previousX, $this->previousY, $aX, $aY);
		}
		$this->previousX = $aX;
		$this->previousY = $aY;
	}

首先,我们设置像素点 ($aX, $aY)。我们还使用 setLabelX() 添加 x 轴下方的标签。如果之前的点 (x, y) 已设置,我们连接这些点以创建折线图。这在 connectTwoPoints() 中分两步完成。首先,对于两个点 (x1, y1)(x2, y2) 之间的所有中间 x 值,我们添加点 (x, y)。给定 x 值的 y 值从以下三角形确定:

如图所示:

y - y1   y2 - y1                           y - y2   x2 - x1
------ = ------- for the left triangle and ------ = ------- for the right triangle
x - x1   x2 - x1                           x2 - x   y1 - y2

从中我们推导出 y,见下代码中的第一个 if

为 x1 和 x2 之间的所有中间点设置 (x, y) 并不足够。假设 x1=10x2=20,但 y1y2 之间存在巨大差异,例如 y1=100y2=150。设置 x 会得到 10 个点,覆盖 y 范围从 100150,形成一条模糊的点线:我们必须在 y1y2 之间的每个 y 值处设置一个点。因此,第二步,我们为 y1 和 y2 之间的所有值推导出 x,见下面的第二个 if

	private function connectTwoPoints($aX1, $aY1, $aX2, $aY2) 
	{
		if ($aY1 < $aY2) {
			for ($x = $aX1 + 1; $x < $aX2; $x++) {
				$y = $aY1 + round(($x - $aX1) * ($aY2 - $aY1) / ($aX2 - $aX1));
				$this->setPixel($x, $y, $this->lineColor);
			}
		} else {
			for ($x = $aX1 + 1; $x < $aX2; $x++) {
				$y = $aY2 + round(($aX2 - $x) * ($aY1 - $aY2) / ($aX2 - $aX1));
				$this->setPixel($x, $y, $this->lineColor);
			}
		}
		if ($aY1 < $aY2) {
			for ($y = $aY1 + 1; $y < $aY2; $y++) {
				$x = $aX1 + round(($y - $aY1) * ($aX2 - $aX1) / ($aY2 - $aY1));
				$this->setPixel($x, $y, $this->lineColor);
			}
		} else {
			for ($y = $aY2 + 1; $y < $aY1; $y++) {
				$x = $aX2 - round(($y - $aY2) * ($aX2 - $aX1) / ($aY1 - $aY2));
				$this->setPixel($x, $y, $this->lineColor);
			}
		}
	}

在前面的代码片段中,有些点会被第一个和第二个 if 都命中;当线的角度为 45 度时,两个 if 都会命中相同的点(柱状四舍五入)。

show() 函数显示图像。

	public function show() 
	{
        ob_start();                      // Begin capturing the byte stream

        imagejpeg($this->gd, NULL, 100); // generate the byte stream

        $rawImageBytes = ob_get_clean(); // and finally retrieve the byte stream

		echo '<img src="data:image/jpeg;base64, '.base64_encode($rawImageBytes).'" />';
	}

剩余的函数处理标签:setMargins()setLabelX()setLabelY()imagestring() 的 x 和 y 参数指的是文本左上角,而 setText() 相对于蓝色矩形左下角进行定位。

结论

这就是构建简单折线图所需的一切。PHP 代码很简单,主要的挑战在于以一致的方式处理 x 和 y。

您可以“按原样”使用 Chart(和 ChartDraw)类,或者改进代码。我没有尝试过的事情:

  • 添加一个选项,将折线图替换为单个数据系列的柱状图:替换 connectTwoPoints()
  • 为“堆叠”柱状图添加代码,即两个(或更多系列)数据,每个柱子的长度是第一个柱子的颜色,第二个柱子的长度在第一个柱子之上,颜色不同:也许使用 imagecolorat() 来查找第一个柱子的顶部?

历史

  • 2020 年 9 月 16 日:初始版本
© . All rights reserved.