纯 PHP 中的折线图
一个可以添加到你的 PHP 页面以创建折线图的类的说明
引言
要在网页上使用 PHP 添加图表,您可以使用像 pChart 这样的工具,但如果您只需要一个简单的折线图,那可能有点过度了。而且,自己动手也很有趣。所以,让我们用纯 PHP 构建一个折线图。您可以在自己的代码中使用 Chart
类,并根据自己的喜好修改 Chart
和 ChartDraw
类;例如,您可能更喜欢柱状图。
折线图包括
- 一个水平 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
标签。 minX
、maxX
、minY
和maxY
可以由Chart
类自身计算。TestChart.php 表明这并不总是您想要的x
值。在Chart
中确定minY
和maxY
需要将所有线条的数据保存在Chart
中,然后计算minY
和maxY
,然后才开始绘制线条。这可以做到,但会使本文的代码不那么清晰。
图表步骤
图表由一个像素矩形组成,在 TestChart.php 中,宽度为 600 像素,高度为 400 像素。这些是图表的物理尺寸,ChartDraw
类只知道这些。但是,我们的数据并不适合这些尺寸:12 个数据元素必须分配到 600 像素上,并且值必须减小以适应 400 像素的高度。这在 Chart
类中完成。
Chart.php 中的前三个函数设置图表的像素尺寸以及 x
和 y
的 min
和 max
值(我以小写的 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
类中,我们使用 width
和 height
来表示像素尺寸。从 $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=10
和 x2=20
,但 y1
和 y2
之间存在巨大差异,例如 y1=100
和 y2=150
。设置 x 会得到 10 个点,覆盖 y 范围从 100
到 150
,形成一条模糊的点线:我们必须在 y1
和 y2
之间的每个 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 日:初始版本