使用 JavaScript 和 ASP.NET 或 PHP 的 AJAX 图表
一个展示如何使用 ASP.NET 或 PHP 创建 JavaScript 图表的例子
介绍
观察实际的股票图表,似乎不使用 Flash、Silverlight 或任何其他插件是不可能绘制 Ajax 图表的。 但是,使用 Ajax 与 canvas 结合应该可以做到。 即使不使用 canvas,也可以显示动态图表,并且它也适用于 Internet Explorer 6.0。
背景
使用 Ajax 图表的一个问题是由于过多的 SQL 请求而导致的高服务器负载,但这可以避免。 在我看来,最简单的方法是使用简单的数据文件。 为了测试目的,我添加了源代码来模拟写入数据文件的服务器。 对于实现,我使用了来自 www.running-charts.com 的免费库。 该库相当小,并且似乎可以很好地用于 Ajax 股票图表。
数据文件
图表的数据文件被实现为一个文本文件。 文本文件中的每一行包含一个值,并用分号分隔,以及相应的时间槽。 我将时间槽而不是时间本身放入数据文件中,因为这减少了要传输到客户端的数据量,而且 - 我懒得在客户端计算时间槽。
请原谅我将数据文件放在与 html 文件相同的位置,但这使得这个例子更容易理解和安装。 在使用代码之前最好将数据文件放在其他地方,因为否则可以直接从 Internet 访问它。
时间槽
时间槽是可以显示在图表中的最小时间单位(1 像素宽)。 例如,如果一个图表是 180 像素宽并且显示 3 小时的数据,那么每个时间槽代表 60 秒的数据。
PHP 图表解决方案
PHP 图表解决方案包含三个小文件:服务器文件 php.php5。 它包含用作数据源的后端函数 ReadData 和模拟将数据写入数据源文件的服务器进程的函数。
<?php
function UpdateTillTimeSlot ($nTimeSlot, $nStartTime)
{
clearstatcache();
$nTimeDiff = time() - $nStartTime;
if ($nTimeSlot <= $nTimeDiff && $nTimeSlot < 360)
{
$datfile = fopen ("data.txt","a+");
while ($nTimeSlot <= $nTimeDiff)
{
$val = sin($nTimeSlot/360*6.28)*10000+10000; // Values between 0 and 20000
$strVal = sprintf ("%d;%d",$val,$nTimeSlot);
fwrite ($datfile,$strVal."\r\n");
$nTimeSlot += 1; // Set increment to 10 to see how it works when there is not much data
if ($nTimeSlot >= 360)
{
fwrite ($datfile,"End\r\n"); // Marker for end
$nTimeSlot = -1;
break;
}
}
fclose ($datfile);
}
return $nTimeSlot;
}
function ReadData(&$oldPos)
{
$result = "";
$fileLength = filesize("data.txt");
$datfile = fopen ("data.txt","r");
$maxwait = 60;
while ($fileLength <= $oldPos && $maxwait-- > 0) // Wait for data
{
sleep(1);
clearstatcache();
$fileLength = filesize("data.txt");
}
if ($fileLength>$oldPos)
{
fseek ($datfile,$oldPos);
$data = fread ($datfile,$fileLength-$oldPos);
$bytesRead = strlen($data);
while ($bytesRead > 0 && $data[--$bytesRead] != "\n") { } //Work only with complete Items
$pos = 0;
while ($pos < $bytesRead)
{
$result .= "<i>";
while ($data[$pos] != "\r")
{
$result .= $data[$pos];
$pos++;
}
$result .= "</i>";
$pos++;
$pos++;
}
if ($data[$bytesRead] == "\n")
{
$bytesRead++;
}
}
$oldPos += $bytesRead;
fclose ($datfile);
return $result;
}
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header('content-type: text/xml; charset=utf-8;',true);
$func=$_POST["func"];
if ($func=="")
{
$func=$_GET["func"];
}
if ($func && $func=="start")
{
$datfile = fopen ("data.txt","w+");
$startTime = sprintf ("%d",time());
fclose ($datfile);
echo "<?xml version='1.0' encoding='UTF-8' ?>";
echo "<Result StartTime='".$startTime."'></Result>";
}
if ($func && $func=="UpdateTillTimeSlot")
{
$timeSlot=$_POST["timeSlot"];
if ($timeSlot=="")
{
$timeSlot=$_GET["timeSlot"];
}
$startTime=$_POST["startTime"];
if ($startTime=="")
{
$startTime=$_GET["startTime"];
}
$timeSlot = UpdateTillTimeSlot ($timeSlot,$startTime);
echo "<?xml version='1.0' encoding='UTF-8' ?>";
echo "<Result TimeSlot='".$timeSlot."'></Result>";
}
if ($func && $func=="ReadData")
{
$oldPos=$_POST["oldPos"];
if ($oldPos=="")
{
$oldPos=$_GET["OldPos"];
}
$result = ReadData($oldPos);
echo "<?xml version='1.0' encoding='UTF-8' ?>";
echo "<Result FilePointer='".$oldPos."'>".$result."</Result>";
}
?>
第二个文件 chart-php.html 可视化来自 php.php5 的 Ajax 驱动的数据流。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<meta http-equiv="Content-Script-Type" content="text/javascript"/>
<meta http-equiv="Content-Style-Type" content="text/css" />
<title>PHP AJAX Chart Sample</title>
<style type="text/css">
.ChartMain { cursor: move }
</style>
<script type="text/javascript" src="chartlib11free.js"></script>
<script type="text/javascript">
/* <![CDATA[ */
var http_request = false;
var g_objFillUpChart;
var g_lastItem;
var g_pos;
function ifDebug()
{
var urlArray = document.URL.split("#");
if (urlArray[1] && urlArray[1]=="D")
{
return true;
}
else
{
return false;
}
}
function GetXmlAttribute(xmlNode,name)
{
if(xmlNode.getAttribute(name))
{
return xmlNode.getAttribute(name);
}
return "";
}
function createAjaxObj()
{
var httpRequest = false
if (window.XMLHttpRequest)
{ // if Mozilla, Safari ...
httpRequest = new XMLHttpRequest()
if (httpRequest.overrideMimeType)
{
httpRequest.overrideMimeType('text/xml')
}
}
else
{
if (window.ActiveXObject)
{ // if IE
try
{
httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
}
}
}
else
{
alert ('Please allow the use of ActiveX (save for scripting)');
}
}
if (!httpRequest)
{
alert('End: (Could not create XMLHTTP-Instance)');
return false;
}
return httpRequest
}
function makeRequest(url,strData,Asynchron,doresult)
{
http_request = createAjaxObj();
http_request.onreadystatechange = doresult;
http_request.open('POST', url, Asynchron);
http_request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http_request.send(strData);
}
function CreateChart(chartDiv,chartType,tXLabel,tYLabel)
{
g_objFillUpChart = new FillUpChart(tXLabel,tYLabel,410,360,220,200,true,new ColorPara ("black","green","red","blue"),chartType,new ComparePara(10000,"yellow",1));
g_objFillUpChart.SetXLabels("gray",10,0,2,"8pt Arial","gray");
g_objFillUpChart.SetXRange(0,359);
g_objFillUpChart.SetXLabel(0,"start");
g_objFillUpChart.SetXLabel(180,"middle");
g_objFillUpChart.SetYLabels(7,"gray",7,10,5,5,"8pt Arial","gray");
g_objFillUpChart.ActualValue(tXLabel,"blue",5,"blue",8,"8pt Arial");
g_objFillUpChart.MaxValue(tXLabel,"green",5,"green",8,"8pt Arial");
g_objFillUpChart.MinValue(tXLabel,"red",5,"red",8,"8pt Arial");
g_objFillUpChart.ScaleY(0,0,0,20000);
var divChart = document.getElementById (chartDiv);
divChart.appendChild(g_objFillUpChart.divNode);
}
function OnUpdateComplete()
{
if (http_request.readyState == 4)
{
if (http_request.status == 200)
{
var bEnd = false;
if (http_request.responseXML.documentElement.firstChild)
{
var element = http_request.responseXML.documentElement.firstChild;
while (element)
{
if (element.nodeType==1 && element.nodeName=="i")
{
var data;
if (element.firstChild.text)
{
data = element.firstChild.text;
}
else
{
data = element.firstChild.textContent;
}
if (data!="End")
{
var arrDat = data.split(";");
g_objFillUpChart.SetPoint(arrDat[1],arrDat[0]);
}
else
{
bEnd = true;
}
}
element=element.nextSibling;
}
}
if (bEnd==false)
{
Update(http_request.responseXML.documentElement.getAttribute("FilePointer"));
}
}
else
{
alert("A problem while requesting: "+http_request.status);
}
}
}
function Update(filePointer)
{
var strData = "func=ReadData";
strData+="&oldPos="+filePointer;
if (ifDebug())
{
alert (strData);
}
makeRequest("php.php5",strData,true,OnUpdateComplete);
}
/* ]]> */
</script>
</head>
<body onload="CreateChart('chart-window-01',ChartType.HighLowActualDiff,-1,-1);Update();">
<div style="position:absolute; top:120px; left:360px; width:600px; font-size:13pt;">
<a id="running-charts.com" style="position:relative; left:27px; font-size:10pt;" href="http://www.running-charts.com/">running-charts.com</a>
</div>
<div id="chart-window-01" style="position:absolute; top:140px; left:360px; width:600px; font-size:13pt;">
</div>
<br/>
</body>
</html>
文件 *updater.html* 模拟一个将数据写入数据源文件的服务器进程。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<meta http-equiv="Content-Script-Type" content="text/javascript"/>
<meta http-equiv="Content-Style-Type" content="text/css" />
<title>PHP Chart-Server Simulation</title>
<script type="text/javascript">
/* <![CDATA[ */
var http_request = false;
var g_startTime;
function ifDebug()
{
var urlArray = document.URL.split("#");
if (urlArray[1] && urlArray[1]=="D")
{
return true;
}
else
{
return false;
}
}
function GetXmlAttribute(xmlNode,name)
{
if(xmlNode.getAttribute(name))
{
return xmlNode.getAttribute(name);
}
return "";
}
function createAjaxObj()
{
var httpRequest = false
if (window.XMLHttpRequest)
{ // if Mozilla, Safari ...
httpRequest = new XMLHttpRequest()
if (httpRequest.overrideMimeType)
{
httpRequest.overrideMimeType('text/xml')
}
}
else
{
if (window.ActiveXObject)
{ // if IE
try
{
httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
}
}
}
else
{
alert ('Please allow the use of ActiveX (save for scripting)');
}
}
if (!httpRequest)
{
alert('End: (Could not create XMLHTTP-Instance)');
return false;
}
return httpRequest
}
function makeRequest(url,strData,Asynchron,doresult)
{
http_request = createAjaxObj();
http_request.onreadystatechange = doresult;
http_request.open('POST', url, Asynchron);
http_request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http_request.send(strData);
}
function OnUpdateDataComplete()
{
if (http_request.readyState == 4)
{
if (http_request.status == 200)
{
if (ifDebug())
{
alert(http_request.responseText);
}
var timeSlot = GetXmlAttribute(http_request.responseXML.documentElement,'TimeSlot');
var state = document.getElementById ("state");
state.innerHTML= timeSlot;
if (-1 != timeSlot)
{
setTimeout(function (){UpdateData(timeSlot,g_startTime);},1000);
}
}
else
{
alert("A problem while requesting: "+http_request.status);
}
}
}
function UpdateData (timeSlot,startTime)
{
var strPara = "func=UpdateTillTimeSlot";
strPara+="&timeSlot="+timeSlot;
strPara+="&startTime="+startTime;
if (ifDebug())
{
alert (strPara);
}
makeRequest("php.php5",strPara,true,OnUpdateDataComplete);
}
function OnStartComplete()
{
if (http_request.readyState == 4)
{
if (http_request.status == 200)
{
if (ifDebug())
{
alert(http_request.responseText);
}
g_startTime = GetXmlAttribute(http_request.responseXML.documentElement,'StartTime');
if (ifDebug())
{
alert(g_startTime);
}
var state = document.getElementById ("state");
state.innerHTML= 0;
setTimeout(function (){UpdateData(0,g_startTime);},1000);
}
else
{
alert("A problem while requesting: "+http_request.status);
}
}
}
function start()
{
var strData = "func=start";
if (ifDebug())
{
alert (strData);
}
makeRequest("php.php5",strData,true,OnStartComplete);
}
/* ]]> */
</script>
</head>
<body onload="start();">
<h1>PHP Chart Data Updater</h1>
<span>Timeslot: </span><span id="state"></span>
</body>
</html>
图表值的来源是一个正弦函数。 因此,在成功将这 3 个文件安装到启用 PHP 5 的服务器后,如果页面 updater.html 也在之前启动,则带有 *chart-php.html* 的页面应显示一个增长的正弦图表。
ASP.NET 图表解决方案
ASP.NET 图表解决方案只有两个文件。 文件 *chart.aspx* 包含服务器端读取数据源和客户端在同一文件中可视化数据。
<%@ Page Language="C#" %>
<%@ Register Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI" TagPrefix="asp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ASP.NET AJAX Chart Sample</title>
<script runat="server">
[System.Web.Services.WebMethod(BufferResponse = false)]
[System.Web.Script.Services.ScriptMethod(ResponseFormat = System.Web.Script.Services.ResponseFormat.Xml)]
public static String ReadData(int nOldPos)
{
int nBytesRead = 0;
String result = "";
using (System.IO.FileStream fileStream = new System.IO.FileStream(HttpContext.Current.Request.MapPath("~/data.txt"), System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite, 1024, false))
{
int nMaxwait = 60;
while (fileStream.Length <= nOldPos && nMaxwait-- > 0) // Wait for data
{
System.Threading.Thread.Sleep(1000);
}
if (fileStream.Length > nOldPos)
{
fileStream.Seek(nOldPos, System.IO.SeekOrigin.Begin);
int nBytes = (int)fileStream.Length - nOldPos;
byte[] data = new byte[nBytes];
nBytesRead = fileStream.Read(data, 0, nBytes);
while (nBytesRead > 0 && data[--nBytesRead] != '\n') { } // Work only with complete Items
int nPos = 0;
while (nPos < nBytesRead)
{
result += "<i>";
while (data[nPos] != '\r')
{
result += (char)data[nPos];
nPos++;
}
result += "</i>";
nPos++;
nPos++;
}
if (data[nBytesRead] == '\n')
{
nBytesRead++;
}
}
}
return "<Result FilePointer='" + (nOldPos + nBytesRead).ToString() + "'>" + result + "</Result>";
}
</script>
<script type="text/javascript" src="chartlib11free.js"></script>
<script type="text/javascript">
var g_objFillUpChart;
function Update(filePointer)
{
PageMethods.ReadData(filePointer, OnUpdateComplete);
}
function OnUpdateComplete(result)
{
var bEnd = false;
if (result.documentElement.firstChild)
{
var element = result.documentElement.firstChild;
while (element)
{
if (element.nodeType==1 && element.nodeName=="i")
{
var data;
if (element.firstChild.text)
{
data = element.firstChild.text;
}
else
{
data = element.firstChild.textContent;
}
if (data!="End")
{
var arrDat = data.split(";");
g_objFillUpChart.SetPoint(arrDat[1],arrDat[0]);
}
else
{
bEnd = true;
}
}
element=element.nextSibling;
}
}
if (bEnd==false)
{
Update(result.documentElement.getAttribute("FilePointer"));
}
}
function CreateChart(chartDiv,chartType,tXLabel,tYLabel)
{
g_objFillUpChart = new FillUpChart(tXLabel,tYLabel,410,360,220,200,true,new ColorPara ("black","green","red","blue"),chartType,new ComparePara(10000,"yellow",1));
g_objFillUpChart.SetXLabels("gray",10,0,2,"8pt Arial","gray");
g_objFillUpChart.SetXRange(0,359);
g_objFillUpChart.SetXLabel(0,"start");
g_objFillUpChart.SetXLabel(179,"middle");
g_objFillUpChart.SetYLabels(7,"gray",7,10,5,5,"8pt Arial","gray");
g_objFillUpChart.ActualValue(tXLabel,"blue",5,"blue",8,"8pt Arial");
g_objFillUpChart.MaxValue(tXLabel,"green",5,"green",8,"8pt Arial");
g_objFillUpChart.MinValue(tXLabel,"red",5,"red",8,"8pt Arial");
g_objFillUpChart.ScaleY(0,0,0,20000);
var divChart = document.getElementById (chartDiv);
divChart.appendChild(g_objFillUpChart.divNode);
}
</script>
</head>
<body onload="CreateChart('chart-window-01',ChartType.HighLowActualDiff,-1,-1);Update(0);">
<h1>ASP.NET AJAX Chart Sample</h1>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" />
</form>
<div style="position:absolute; top:120px; left:270px; width:600px; font-size:13pt;">
<a id="running-charts.com" style="position:relative; left:27px; font-size:10pt;" href="http://www.running-charts.com/">running-charts.com</a>
</div>
<div id="chart-window-01" style="position:absolute; top:140px; left:270px; width:600px; font-size:13pt;">
</div>
</body>
</html>
文件 updater.aspx 包含模拟将数据写入数据源的服务器的服务器端和同一文件中相应的客户端界面。
<%@ Page Language="C#" %>
<%@ Register Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="System.Web.UI" TagPrefix="asp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>ASP.NET AJAX Web Services: Web Service Sample Page</title>
<script runat="server">
[System.Web.Services.WebMethod(BufferResponse = false)]
[System.Web.Script.Services.ScriptMethod()]
public static long UpdateTillTimeSlot(long nTimeSlot, long nStartTime)
{
System.DateTime dateTime = System.DateTime.Now;
System.DateTime date1970 = new System.DateTime(1970, 1, 1, 0, 0, 0);
System.TimeSpan timeDiff = dateTime.Subtract(date1970);
long nTimeDiff = (int)timeDiff.TotalSeconds - nStartTime;
if (nTimeSlot <= nTimeDiff && nTimeSlot < 360)
{
using (System.IO.FileStream fileStream = new System.IO.FileStream(HttpContext.Current.Request.MapPath("~/data.txt"), System.IO.FileMode.Append, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite, 1024, false))
{
System.Text.ASCIIEncoding encBytes = new System.Text.ASCIIEncoding();
while (nTimeSlot <= nTimeDiff)
{
double dSin = Math.Sin(nTimeSlot * 6.28 / 360) * 10000 + 10000;
int nSin = ((int)dSin);
byte[] bytes = encBytes.GetBytes(nSin.ToString("D") + ";" + nTimeSlot.ToString("D") + "\r\n");
fileStream.Write(bytes, 0, bytes.Length);
nTimeSlot += 1; // Set increment to 10 to see how it works when there is not so much data.
if (nTimeSlot >= 360)
{
fileStream.Write(new byte[] { (byte)'E', (byte)'n', (byte)'d', (byte)'\r', (byte)'\n' }, 0, 5); // Marker for end
nTimeSlot = -1;
break;
}
}
}
}
return nTimeSlot;
}
[System.Web.Services.WebMethod(BufferResponse = false)]
[System.Web.Script.Services.ScriptMethod()]
public static long Reset()
{
System.DateTime dateTime = System.DateTime.Now;
System.DateTime date1970 = new System.DateTime(1970, 1, 1, 0, 0, 0);
System.TimeSpan timeDiff = dateTime.Subtract(date1970);
using (System.IO.FileStream fileStream = new System.IO.FileStream(HttpContext.Current.Request.MapPath("~/data.txt"), System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite, 1024, false)) {}
return (int)timeDiff.TotalSeconds;
}
</script>
<script type="text/javascript">
var g_timeSlot;
var g_startTime;
function Start()
{
PageMethods.Reset(OnStartComplete);
}
function OnStartComplete(result)
{
var state = document.getElementById ("state");
state.innerHTML= 0;
g_timeSlot = 0;
g_startTime = result;
setTimeout(function (){UpdateData();},1000);
}
function UpdateData()
{
PageMethods.UpdateTillTimeSlot(g_timeSlot, g_startTime, OnUpdateDataComplete);
}
function OnUpdateDataComplete(result)
{
var state = document.getElementById ("state");
state.innerHTML= result;
if (-1 != result)
{
g_timeSlot = result;
setTimeout(function (){UpdateData();},1000);
}
}
</script>
</head>
<body onload="Start();">
<h1>ASP.NET AJAX Chart Data Updater</h1>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" />
</form>
<span>Timeslot: </span><span id="state"></span>
</body>
</html>
与 php 解决方案一样,必须先打开页面 updater.asp 才能使用页面 chart.aspx。
对于此 ASP.NET 解决方案,需要 ASP.NET 4.0。
结论
我认为这篇文章表明:无需图片和插件即可显示 Ajax 股票图表。 而且它相当简单! 唯一的风险是通过 Ajax 连接的网络流量。 但如果需要实际值,这是不可避免的。 如果不需要实际值,只需要静态图表,那么使用 JavaScript 和 CSS 而不是图表图片显示图表甚至可以避免网络流量,因为 JavaScript 图表可以通过第一次调用互联网页面来传输。 不需要第二次 http 调用来获取图表图片。