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

动态 JQPlot

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2013 年 2 月 12 日

CPOL

8分钟阅读

viewsIcon

42346

downloadIcon

1069

如何通过后台代码动态构建 JQPlot 图。

引言

本文介绍了如何构建一个可以根据 JQPlot 库动态构建图表的 Web 应用程序。

虽然代码是用 Java 编写的,但可以轻松地移植到 C#、C++、VB 或任何其他语言。

工具和库

创建此项目使用了以下工具

源代码

本文基于一个名为 Staff 的真实应用程序,您可以在此处查看: http://www.staff.oma.be ,因此可以部分视为一个案例研究。查看此网站并执行“查看源代码”可能会有所帮助。该网站绘制与太阳物理学相关的数据时间线。

然而,为了让您入门,我已经将 Staff 应用程序精简到了最基本的状态。

背景

目标是创建一个能够显示时间线数据的应用程序,并满足四个约束条件:

  1. 需要处理大量数据
  2. 需要快速
  3. 需要可访问(面向所有人)
  4. 需要易于使用

这些约束条件带来了初始问题,因为可访问性和大量数据与速度相悖。任务:构建一个能够处理数十亿条记录的 Web 应用程序。

请注意,尽管本文具有时间线特性(1个值,1个日期时间戳),但代码也可用于其他类型的图表。

使用代码

基本上,有两种工作流程汇集在一起。一种是构建 JSON 代码本身来生成图表,另一种是从数据库中提取数据并将其放入图表。

1. 构建 JSON 代码

这部分很简单,只需复制 JQPlot 的属性(在此处找到: 此处),并使用标准的 getter 和 setter 为它们构建类。

这些类可以帮助您走得很远:

  • GraphAxes
  • GraphCanvasOverlay
  • GraphCursor
  • GraphGrid
  • GraphHighlighter
  • GraphLegend
  • GraphSeries

注意:并非 JQPlot API 的所有属性都已包含在内。但它们很容易添加。我通常会根据 JQPlot 示例进行原始 HTML 试运行,然后从中逐步构建。

为了安全起见,我将一些属性转换为枚举,但大多数属性保持原样。每个类最显著的特性是重写了 toString 方法,该方法将对象作为 JSON 代码返回。为了组合所有对象,我还为 Graph 类创建了一个重写的 toString 方法。

使用代码仅限于两个步骤:

  1. 设置您想要的所有属性。
  2. 使用 Graph 的 toString 方法将 JSON 写入您的网页。

再简单不过了。

public class Graph {
    private String graphtitle = "";
    private ArrayList<GraphSeries> graphseries;
    private ArrayList<GraphAxes> graphaxes;
    private GraphLegend graphlegend;
    private GraphCanvasOverlay graphcanvasoverlay;
    private GraphHighLighter graphhiglighter;
    private GraphCursor graphcursor;
    private GraphGrid graphgrid;
 
//getters - setters here

    @Override
    /** returns the complete string javascript representation for 1 JQPlot object */
    public String toString(){
        StringBuilder builder = new StringBuilder("optionsObj = 
          $.extend(true, {}, { title: '"+ graphtitle +"',\r\n");
        
        //write the series part
        if(graphseries != null){
            builder.append("series:[");
            for(int i = 0; i < graphseries.size(); i++){
                GraphSeries gs = graphseries.get(i);
                if(gs != null){
                    builder.append(gs.toString());
                }
            }
            //remove the final ','
            builder.delete(builder.length()-1, builder.length());
            builder.append("]");
            builder.append(",\r\n");
        }
        
        
        //write the axes part
        if(graphaxes != null){
            builder.append("axes:{");
            for(int i = 0; i < graphaxes.size(); i++){
                GraphAxes ga = graphaxes.get(i);
                if(ga != null){
                    builder.append(ga.toString());
                }
            }
            //remove the final ','
            builder.delete(builder.length()-1, builder.length());
            builder.append("},\r\n");
        }
        
        //write the grid
        if(graphgrid != null){
            builder.append("grid:{");
            builder.append(graphgrid.toString());
            builder.append("},");            
        }
        
        //write the legend
        if(graphlegend != null){
            builder.append("legend:{");
            builder.append(graphlegend.toString());
            builder.append("},");
        }
        
        //write the canvasoverlay
        if(graphcanvasoverlay != null){
            builder.append("canvasOverlay:{");
            builder.append(graphcanvasoverlay.toString());
            builder.append("},\r\n");
        }
        
        //write highlighter properties
        if(graphhiglighter != null){
            builder.append("highlighter:{");
            builder.append(graphhiglighter.toString());
            builder.append("},\r\n");
        }
        
        //write cursor properties
        if(graphcursor != null){
            builder.append("cursor:{");
            builder.append(graphcursor.toString());
            builder.append("}\r\n");
        }
        builder.append("});");
        
        return builder.toString();
    }  

Graph 类包含几个直接与 JQPlot 库每个对象相关的对象。每个类都有自己重写的 toString 方法来输出 JSON 代码的该部分。Graph 类将所有内容绑定在一起。我避免使用过多的布局(空格字符),从而节省了一些要发送的字节。数据集可能会变得很大。

2. 获取数据。

这部分将根据您的具体需求而有所不同。但是,我将解释我们是如何能够绘制如此大量数据的。

我们这里推理的起点是我们拥有海量数据(其中一张表包含超过 1.8x109 条记录)。如果有人选择了整个时间范围内的这些数据,那么,就放弃吧。

即使尝试显示如此多的点也是徒劳的。我的笔记本屏幕分辨率为 1920x1080,但许多屏幕只有 1024x768 或更低。简单地说:显示超过,比如说 2000 个点,是毫无意义的。有人可能会争辩说可以使用滚动条来绘制窗口外的部分图表,但这对我来说,违背了约束 #4,即易用性。

解决方案是将数据降采样到不同的级别。我们的大部分数据是每分钟 1 个数据点。(并非全部,这使得它相当复杂,但为了不让事情变得太困难,让我们假设每分钟 1 个数据点)。这意味着降采样到不同的“较低分辨率”时间范围,例如小时、天、月、年。

在我们的例子中,我们使用了标准平均值,但更复杂的计算(包括误差范围和显示它们)当然也是可能的。降采样的难点在于超出正常时间范围时,例如 5 分钟平均值和卡林顿自转

另一个难点是,当数据上传时,一些数据点被标记为“不正确”。我们将这些数据导入到最高分辨率的数据中,但不想让它们弄乱平均值。在降采样时,只接受完整的数据点。例如,当我们要创建每小时降采样表时,您需要确保有 60 个 1 分钟表中的点可用于计算该平均值。

在我们的例子中,数据也是近实时导入的,其中每个源,这一点很重要,都以或多或少相同的方式重构,这使得您的代码设计更加直接。近实时导入是一个独立的模块,作为 Linux 服务器上的 cron 作业运行。

最后,不言而喻,良好的数据库结构和索引很重要。我们的数据库是 PostGreSql 数据库,但 MySQL、SQL-Server 和 Oracle 也可以。

3. 将数据和图表放在一起

将数据库中的数据放入图表也证明是一项相当大的挑战。

为了确定从哪个(降采样)表中选择数据,会获取开始和结束日期时间并计算差值。通过一些元数据(其中包含该时间线的最高分辨率等信息),可以找到降采样级别。

例如,如果您的最高采样率为每分钟 1 个数据,并且您想在一个系列中绘制最多 2000 个点,那么如果时间差超过 2000 分钟或 33 小时,您应该采用每小时平均值(而不是每分钟数据)。如果您超过 2000 小时,您应该采用每日平均值等...(请注意,2000 小时已经是 120,000 条每分钟数据记录)。

第二点是,一些数据系列需要相同的坐标轴,而另一些则需要不同的坐标轴。根据您自己的业务规则来组合正确的系列和正确的坐标轴,但幸运的是,通过第一步的 Graph 类可以实现。在我的例子中,我使用了一个 GraphBuilder 对象来根据选定的数据构建 Graph 对象。

这是示例源代码中的代码。这是绘制同一坐标轴上两个系列的最简单方法。要开始使用,您可能想尝试将这两个系列绘制在两个不同的坐标轴上,而不是一个,然后在此基础上逐步处理更复杂的情况。

/** Builds the Graph object from the selected data. */
public Graph Build(ArrayList<DataPoint> seriesa, 
  ArrayList<DataPoint> seriesb, GregorianCalendar start, GregorianCalendar end){
    Graph graph = new Graph();

    ArrayList<GraphAxes> al_axes = new ArrayList<GraphAxes>();
    ArrayList<GraphSeries> al_series = new ArrayList<GraphSeries>();
    
    GraphCanvasOverlay gco    = getGraphCanvasOverlay();
    GraphCursor gc        = getGraphCursor();
    GraphHighLighter ghl    = getGraphHighLighter();
    GraphLegend gl        = getGraphLegend();
    GraphGrid gg        = getGraphGrid();
    GraphAxes xaxis        = getGraphXAxes(start, end);
    al_axes.add(xaxis);
    
    GraphSeries serie = getGraphSeries("Series A", 0);
    al_series.add(serie);
    serie = getGraphSeries("Series B", 0);
    al_series.add(serie);
    GraphAxes yaxis = getGraphYAxes(0, "Calculated Value");
    al_axes.add(yaxis);
    
    addGraphCanvasOverlayObjects(gco, 0);

    graph.setGraphTitle("Codeproject JQPlot example");
    graph.setAxes(al_axes);
    graph.setSeries(al_series);
    graph.setLegend(gl);
    graph.setCanvasOverlay(gco);
    graph.setGraphCursor(gc);
    graph.setGraphGrid(gg);
    graph.setHighlighter(ghl);

    return graph;
}

最后,您需要写出 JavaScript,其中不仅包含图表的 JSON 部分,还包含用于实际数据的 JavaScript 数组以及用于放置绘图区域的 HTML。

这是基本 HTML 的样子:

<%
// This part loads the PageBuilder object.
PageBuilder pb = new PageBuilder();
%> 
<script><%= pb.WritePlotData() %></script>
<div id="main">
    <div class="plotcanvas">
    <div id="plotarea">
        <!-- This is the actual plotting area. -->
        <div id="chart" class="graph"></div>    
    </div>
    </div>
</div>
<script type="text/javascript" class="code">
    <%= pb.WriteGraphDefinition() %>
    $(document).ready(function(){
        CreateGraphAsync();
    });
</script>

WritePlotDataWriteGraphDefinition 函数仅生成并“输出” HTML 和 JavaScript。

4. 完善细节

根据我的经验,即使拥有一个漂亮、闪电般快速的架构,如果用户界面丑陋、难用或两者兼而有之,应用程序仍然可能毫无吸引力。这就是我们有易用性约束的原因。

为了解决这个问题,我们考虑了用户界面的一些规则:

  • 在整个页面中,外观和感觉必须保持一致。
  • 考虑到我们已经不是 256 色时代,而且没有人对彩虹色网页感兴趣(相信我,它们存在,而且很糟糕),它应该看起来不错。
  • 进入页面时,最重要的部分应该显而易见,吸引用户的注意力。(在此情况下是图表)
  • 经常使用的用户控件应该靠近并随时可用,高级、不常用的功能可以隐藏在菜单中。

结论

我们开始的四个约束条件已得到满足:

  • 大量数据
  • 庞大且仍在增长的数据库包含数十亿条记录,而应用程序的性能没有明显下降。

  • 速度
  • 尽管是 Web 应用程序,但加载速度在可接受的范围内。这是由于对大数据集进行了有效的降采样处理,以及设计简洁,将网页视为“文本片段”。此外,客户端脚本用于许多控件,限制了到服务器的回发。在我们的应用程序中,唯一的回发发生在需要获取新数据集时。

  • 可访问性
  • 只要有互联网连接,该网页对每个人都是可用的。

  • 可用性
  • 该网页非常易于使用。

关注点

基本的观点是,您的 Web 浏览器只不过是一个文本解释器,与 Notepad++ 或 Word 没有区别。这就是我大量使用 StringBuilder 的原因,因为它非常高效。

本文的真实应用程序名为 Staff,第一个版本可以在此处在线找到: 此处。示例代码是真实应用程序的简化版本,真实应用程序的复杂性要大得多,但我也使用了更好、更干净的编码技术(防御性编程、异常处理、日志记录等)。我特意将本文的代码量降到了最低。

注意:我更多的是 .NET 背景,对 Java 的经验不多。很有可能存在其他解决某些问题的方案。然而,Staff 应用程序运行良好,并且由于其出色的设计(对于所遇到问题的复杂性来说,它出奇地简单),因此非常快速。

历史

  • 版本 1:2013 年 2 月 12 日。
© . All rights reserved.