使用过ReportView的人都知道,在图表控件日新月异的今天,微软的ReportView自带的的图表控件早已经明显不能满足当今人们对于报表前端超炫展示的需求了,那么,构建一个比较炫,又比较好用的报表开发解决方案已经显得越来越重要了。但是,图表控件又不能实现微软ReportViewer里边的表格与矩阵的强大功能,所以,我尝试性的将微软的ReportViewer与图表控件结合起来使用,取两者之所长,来实现交互式的报表开发。下边,我将简单的介绍一下整个解决方案,及其关键技术:
在该解决方案中,经过权衡,我采用的是js的图表控件amCharts和HighCharts,代码管理是VS2010,数据库是SqlServe r2008 r2,并且使用了AnalysisServices,它是微软SqlServe r2008 r2自带的数据仓库工具。
先看下界面,大家一睹为快吧:
上边的图表都是有交互效果的,类似于flash动画的效果。并且点击左边表格里边的数据行(蓝色字体),右边的图表和下边的条形图都会出现联动。
前端代码:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
<script type="text/javascript" src="Scripts/js/highcharts.js"></script>
<script type="text/javascript" src="Scripts/js/modules/exporting.js"></script>
<script type="text/javascript">
var chart;
var chart2;
$(document).ready(function () {
//addDataToChart2(data);
// addDataToChart(xdata, ydata)
});
function addDataToChart(xdata,ydata) {
chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
defaultSeriesType: 'column'
},
title: {
text: '渠道分析'
},
subtitle: {
text: '按招聘职位'
},
xAxis: {
categories: xdata
},
yAxis: {
min: 0,
title: {
text: '人数'
}
},
legend: {
layout: 'vertical',
backgroundColor: '#FFFFFF',
align: 'left',
verticalAlign: 'top',
x: 100,
y: 150,
floating: true,
shadow: true
},
tooltip: {
formatter: function () {
return '' +
this.x + ': ' + this.y + ' 人';
}
},
plotOptions: {
column: {
pointPadding: 0.2,
borderWidth: 0
}
},
series: [{
name: '人数',
data: ydata
}]
});
}
function addDataToChart2(data) {
chart2 = new Highcharts.Chart({
chart: {
renderTo: 'containerpie',
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false
},
title: {
text: '各渠道来源比例'
},
tooltip: {
formatter: function () {
return '<b>' + this.point.name + '</b>: ' + Fractional(this.percentage) + ' %';
}
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
formatter: function () {
return '<b>' + this.point.name + '</b>: ' + Fractional(this.percentage) + ' %';
}
}
}
},
series: [{
type: 'pie',
name: 'Browser share',
data: data
}]
});
}
</script>
上边的代码是负责画图的,下边的代码是负责Ajax请求与数据处理的:
<table>
<tr style=" height:450px;">
<td style="width:400px;">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<rsweb:ReportViewer ID="ReportViewer1" runat="server" Font-Names="Verdana"
Font-Size="8pt" InteractiveDeviceInfos="(集合)" WaitMessageFont-Names="Verdana"
WaitMessageFont-Size="14pt"
ondrillthrough="ReportViewer1_Drillthrough" ShowToolBar="False">
<LocalReport ReportPath="Report1.rdlc">
</LocalReport>
</rsweb:ReportViewer>
</td>
<td style="width:450px;">
<div id="containerpie" style="width: 500px; height: 400px; margin: 0 auto"></div>
</td>
</tr>
<tr>
<td colspan="2">
<div id="container" style="width: 900px; height: 400px; margin: 0 auto"></div>
</td>
</tr>
</table>
<script type="text/javascript" language="javascript">
function reset(jobid) {
$.ajax({
type: "POST",
url: "/Data/SourceData.ashx",
data: "key=" + jobid + "",
error: function (textStatus) {
alert(textStatus.status);
},
success: function (msg) {
//刷新chart
msg = msg.substring(1, msg.length - 1);
msg = msg.split('≮');
xdata = msg[0];
ydata = msg[1];
pdata = msg[2];
xdata = eval("(" + xdata + ")");
ydata = eval("(" + ydata + ")");
pdata = eval("(" + pdata + ")");
addDataToChart(xdata, ydata);
addDataToChart2(pdata);
}
});
}
//小数位数控制,可以四舍五入
function Fractional(n) {
//小数保留位数
var bit = 2;
//加上小数点后要扩充1位
bit++;
//数字转为字符串
n = n.toString();
//获取小数点位置
var point = n.indexOf('.');
//n的长度大于保留位数长度
if (n.length > point + bit) {
//保留小数后一位是否大于4,大于4进位
if (parseInt(n.substring(point + bit, point + bit + 1)) > 4) {
return n.substring(0, point) + "." + (parseInt(n.substring(point + 1, point + bit)) + 1);
}
else {
return n.substring(0, point) + n.substring(point, point + bit);
}
}
return n;
}
</script>
看到这里大家一定会有疑问,Ajax请求里边怎样实现访问数据库的,这里我是建了一个SourceData.ashx文件,实现Ajax的数据处理并返回的:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using Newtonsoft.Json;
namespace AnalysisServicesDemo.Data
{
/// <summary>
/// SourceData 的摘要说明
/// </summary>
public class SourceData : IHttpHandler
{
private string DBConnectionString = Config.CommonConn2;
public void ProcessRequest(HttpContext context)
{
int jobid = Int32.Parse(context.Request["key"]);
DataTable dt = GetDrillData(jobid);
string categories = "[";
string value = "[";
string pievalue = "[";
for (int i = 0; i < dt.Rows.Count; i++)
{
categories += "'" + dt.Rows[i][0].ToString() + "',";
value += (dt.Rows[i][1].ToString() == "" ? "0" : dt.Rows[i][1].ToString()) + ",";
pievalue += "[" + "'" + dt.Rows[i][0].ToString() + "'," + dt.Rows[i][1].ToString() + "]"+",";
}
value = value.Substring(0, value.Length - 1);
categories = categories.Substring(0, categories.Length - 1);
pievalue = pievalue.Substring(0, pievalue.Length - 1);
value += "]";
categories += "]";
pievalue += "]";
string result = categories + "≮" + value + "≮" + pievalue;
context.Response.Write(JsonConvert.SerializeObject(result));
}
public bool IsReusable
{
get
{
return false;
}
}
protected DataTable GetDrillData(int jobid)
{
string SQLQuery = @"WITH x AS (SELECT DISTINCT
rsl.TenantId ,
rsl.JobId ,
job.JobTitle ,
ds.sourcename ,
SUM(rsl.total) AS total
FROM dbo.report_sourcetotal_last rsl
JOIN dbo.Recruitment_Job job ON job.JobId = rsl.Jobid
JOIN dbo.DiffSource ds ON ds.Sourceid = rsl.Source
WHERE rsl.TenantId = 110006 AND rsl.jobid={0}
GROUP BY ds.sourcename ,rsl.TenantId ,
rsl.JobId ,
job.JobTitle ,
rsl.totalitemid )
SELECT x.sourcename,SUM(x.total)AS total FROM x
GROUP BY x.sourcename,x.total";
string sql = string.Format(SQLQuery, jobid);
if (DBConnectionString == null || sql == null)
{
throw new Exception("Please provide either DataRows, or both DBConnectionString and SQLQuery to get data");
}
DataSet ds = new DataSet();
SqlConnection conn = new SqlConnection(DBConnectionString);
conn.Open();
SqlDataAdapter sda = new SqlDataAdapter(sql, conn);
sda.Fill(ds);
conn.Close();
return ds.Tables[0];
}
}
}
完成上边的所有工作之后,现在需要开发rdlc文件了,这里是简单的开发了一个rdlc报表文件。特别需要说明的是,该文件需要设置一个参数ReportParameter1,用来获取你所点击的JobTitle行的参数,也就值需要知道你点击的是哪一行,然后把这个参数传给前端的Js函数,由他去进行Ajax请求。数据钻取方法:
protected void ReportViewer1_Drillthrough(object sender, DrillthroughEventArgs e)
{
LocalReport lp = (LocalReport)e.Report;
string jobid = lp.GetParameters()["ReportParameter1"].Values[0].Trim();
lp.DataSources.Clear();
DataTable dt = GetData();
lp.DataSources.Add(new ReportDataSource("DataSet1", dt));
ScriptManager.RegisterStartupScript(this.Page, typeof(string), "addV", "reset(" + jobid + ");", true);
}
其中:
ScriptManager.RegisterStartupScript(this.Page, typeof(string), "addV", "reset(" + jobid + ");", true);
是实现服务器端访问客户端的JS函数并且传参数
jobid给它的。这样后端的代码就可以访问前端的JS了。
上边的rdlc文件是要在ReportViewer里边展现的,在它的参数里边设定了一个名称为ReportParameter1的参数,并且允许参数为null和空。
到这时候,运行程序,点击左边的ReportViewer里边的报表(表格或矩阵)里边的数据钻取的行,就可以实现图表饼状图与柱状图的数据的联动了。
what's more?
当然了,要是你想使用AnalysisServices里边的数据源,可以使用以下的方法,在使用AnalysisServices之前需要配置AnalysisServices的Http远程访问,配置及使用方法,请参考我的另外一篇文章:
关于如何远程访问AnalysisServices以及Cell向DataTable转换的关键技术
下边举个例子,是Ajax的数据处理文件ashx请求AnalysisServices的例子:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Newtonsoft.Json;
using Microsoft.AnalysisServices.AdomdClient;
using System.Data;
namespace AnalysisServicesDemo.Data
{
/// <summary>
/// MonthData 的摘要说明
/// </summary>
public class MonthData : IHttpHandler
{
private string DBConnectionString = Config.CommonConn;
private int s = 0;
public void ProcessRequest(HttpContext context)
{
string name = context.Request["key"];
DataTable dt = GetData();
for (int i = 0; i < dt.Rows.Count; i++)
{
if (dt.Rows[i][0].ToString() == name)
{
s = i;
}
}
string result="[";
for (int i = 2; i < dt.Columns.Count; i++)
{
result += (dt.Rows[s][i].ToString() == "" ? "0" : dt.Rows[s][i].ToString()) + ",";
}
result = result.Substring(0,result.Length-1);
result += "]";
context.Response.Write(JsonConvert.SerializeObject(result));
}
public bool IsReusable
{
get
{
return false;
}
}
protected DataTable GetData()
{
string SQLQuery = @"SELECT
NON EMPTY Hierarchize({DrilldownLevel({[Wd Highest Degree].[Name].[All]})})
ON COLUMNS , NON EMPTY
Hierarchize(DrilldownMember({{DrilldownLevel({[Wd Datetime].[Hierarchy].[All]})}},
{[Wd Datetime].[Hierarchy].[D Year].&[2010],[Wd Datetime].[Hierarchy].[D Year].&[2011]}))
ON ROWS
FROM [Beisen ASA]
WHERE ([Measures].[Fact Recuritment Job Count])";
if (DBConnectionString == null || SQLQuery == null)
{
throw new Exception("Please provide either DataRows, or both DBConnectionString and SQLQuery to get data");
}
AdomdConnection conn = new AdomdConnection(DBConnectionString);
AdomdCommand comm = new AdomdCommand(SQLQuery, conn);
conn.Open();
CellSet cs = comm.ExecuteCellSet();
conn.Close();
return CellSetToDataTable.ToDataTable(cs, "table0");
}
}
}
下边是AnalysisServices的数据访问的字符串: public static readonly string CommonConn = "Provider=MSOLAP;Data Source=http://localhost/olap/msmdpump.dll; Initial Catalog=Recuritment_job;";
上边的方法里边,用到了一个把CellSet转换为DataTable的方法,因为AnalysisServices里边查询出来的数据格式是CellSet,而我们数据绑定需要DataTable,所以这里进行了一些处理,下边将该方法的详细地实现,粘在这里与大家分享:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AnalysisServices.AdomdClient;
using System.Data;
namespace AnalysisServicesDemo
{
public class CellSetToDataTable
{
/// <summary>
/// 将CellSet转化成DataTable(包括所有维度)
/// </summary>
/// <param name="cs">CellSet</param>
/// <returns></returns>
public static DataTable ToDataTable(CellSet cs, string tbname)
{
DataTable dt = new DataTable(tbname);
DataColumn dc = null;
DataRow dr = null;
//生成数据列对象
//多个维度转化成列
for (int col = 0; col < cs.Axes[1].Set.Hierarchies.Count; col++)
{
dc = new DataColumn();
//下面的代码会报错:"The connection is not open.” 获取层次结构的维度名时需要连接Cube才可以!
//dt.Columns.Add(new DataColumn(cs.Axes[1].Set.Hierarchies[col].ParentDimension.Name));
dt.Columns.Add(new DataColumn("Dimension" + col.ToString()));
}
int index = 0;
foreach (Position p in cs.Axes[0].Positions)
{
dc = new DataColumn();
string name = "";
foreach (Member m in p.Members)
{
name += m.Caption + "-";
}
if (name.Length > 0)
{
name = name.Substring(0, name.Length - 1);
}
//这里防止维度成员或度量值重名而需要容错处理
try
{
dc.ColumnName = name;
dt.Columns.Add(dc);
}
catch (System.Exception ex)
{
dc.ColumnName = name + index.ToString();
dt.Columns.Add(dc);
}
index++;
}
//添加行数据
int pos = 0;
foreach (Position py in cs.Axes[1].Positions)
{
dr = dt.NewRow();
//维度描述列数据
int cols = 0;
foreach (Member m in py.Members)
{
dr[cols] = m.Caption;
cols++;
}
//数据列
for (int x = 1; x <= cs.Axes[0].Positions.Count; x++)
{
dr[x + cols - 1] = cs[pos++].FormattedValue;
}
dt.Rows.Add(dr);
}
return dt;
}
}
}
Besides……
两款比较好的JS图表控件:
HighCharts:http://www.highcharts.com/demo/area-negative
AmCharts:http://www.amcharts.com/bundle/bars-with-gradient-fills/
Flash控件:
Fusioncharts:http://www.fusioncharts.com/
感兴趣的话,可以留言交流,留言交流的多的话,会激励我继续更新,并提供相关的源码!