今天主要总结一下JfreeChart中热点map的应用,根据我个人的理解来总结的,可能会有某些地方不是很正确,如有错误,请大家指正,不断改进。
首先,这里的热点的应用在于用户与JfreeChart图像的交互,也就是与图像文件(因为我们是将Chart对象以二进制数据写入一个图像文件了,比如PNG文件,JPEG图像文件)交互。在HTML中,为了让一个图像具有可交互的功能就必须给该图像定义一个Map对象,然后在
<img id="chartimage" src="${linechart }" usemap="#${filename }" >
也就是在img标签的usemap属性上,指定map对象。
下面举例说明,用JfreeChart在这个图像中产生热点。
那么它的原始map数据大致为:
<map id="jfreechart-23846.png" name="jfreechart-23846.png">
<area shape="poly" coords="675,93,681,93,681,99,675,99,675,93,675,93" title="日平均攝取熱量: (09-12-28 上午12:00, 1,788)" alt=""/>
<area shape="poly" coords="276,96,282,96,282,102,276,102,276,96,276,96" title="日平均攝取熱量: (09-11-27 上午12:00, 1,741.8)" alt=""/>
<area shape="poly"
<area shape="poly" coords="225,97,231,97,231,103,225,103,225,97,225,97" title="日平均攝取熱量: (09-11-23 上午12:00, 1,722.4)" alt=""/>
<area shape="poly" coords="115,24,114,26,112,27,110,26,109,24,110,22,112,21,114,22,115,24,115,24" title="日平均消耗熱量: (09-11-14 上午12:00, 3,027.8)" alt=""/>
</map>
这里面,shape是指热点的区域,本段中的poly是一个多边形,也有Rectangle等。Coords是这个多边形的坐标,通常是需要用工具生成,这里所展示的是JfreeChart自动生成的。Title=””中的内容便是mouse移到各个点时,展示出的信息,mouse移开后信息不再显示。这里不需要我们再写别的代码,个人理解是,浏览器可以解析这部分数据,相当于将title的内容在onmouseover中显示。
那么,现在的问题就在于,怎么获得map数据了。
当然是根据一个图像来生成对应的MAP对象。
在创建一个图表对象时候有两个参数,比如这里面的折线图的部分代码:
JFreeChart jfreechart = ChartFactory.createTimeSeriesChart(title, "",
y, xydataset, true, true, false);
两个参数就是这个方法中的最后两个参数,这两个参数的类型都是布尔值。这两个参数意思分别是:是否创建工具提示(tooltip)以及是否生成URL。这两个参数分别对应着MAP中一个AREA的title属性以及href属性。
可是我想知道的是怎么来产生这个MAP啊!哈哈,不要着急,JFreeChart已经帮我们做好生成MAP对象的功能。为了生成MAP对象就要引入另外一个对象:ChartRenderingInfo。因为JFreeChart没有直接的方法利用一个图表对象直接生成MAP数据,它需要一个中间对象来过渡,这个对象就是ChartRenderingInfo。下图是生成MAP数据的流程图:

如上图所示,ChartUtilities类是整个流程的核心,它周围的对象都是一些例如数据对象或者是文件等。这个流程简单描述如下:首先创建一个ChartRenderingInfo对象并在调用ChartUtilities的writeChartAsJPEG时作为最后一个参数传递进去。调用该方法结束后将产生一个图像文件以及一个填充好MAP数据的ChartRenderingInfo对象,有了这个对象我们还是没有办法获取具体的MAP数据,我们还必须借助于ChartUtilities的writeImageMap方法来将ChartRenderingInfo对象读取出来,获取MAP数据的代码片断如下:
JFreeChart chart = createCaloriesLineChart(timesers, weekormonth,
title, avgenergy + "(" + y + ")", index, week, year1,year,
nodatamess, maxcalorie, month);
HttpServletRequest request = io.getRequest();
String filename = "";
String graphURL = "";
PrintWriter writer = null;
try {
io.getResponse().setContentType("text/html");
io.getResponse().setCharacterEncoding("UTF-8"); // 这里要设置一下,因为这里相当于用PrintWriter将图片写出来,如果不设置字符集,则默认为ISO-8859-1,而页面用的是UTF-8.
writer = io.getResponse().getWriter();
Shape shape = new Rectangle(10, 5);
ChartEntity entity = new ChartEntity(shape);
StandardEntityCollection coll = new StandardEntityCollection();
coll.add(entity);
ChartRenderingInfo info = new ChartRenderingInfo(coll);
filename = ServletUtilities.saveChartAsPNG(chart, 720, 250, info,
io.getSession());
ChartUtilities.writeImageMap(writer, filename, info, true);
graphURL = request.getContextPath() + "/displayChart?filename="
+ filename;
String strimg = ChartUtilities.getImageMap(filename, info);
当初在实现这个拿到map数据时,花费了不少时间,根据writeImageMap()回溯到找寻如何得到 info,要得到info又如何得到StandardEntityCollection对象,以及StandardEntityCollection与ChartEntity、Shape的关联,这个就需要花时间好好研究API了,感觉JfreeChart API写的太过于简单,需要自己动手试试一些方法才可以完成,不过感觉这部分还是比较单一的需求,感觉在图象对象其他的类上,过于复杂,文档过于简单,我们必须去了解每个类型的图表对象应该对应哪些Axis、Plot、Renderer类,并且必须非常熟悉这些类的构造函数中每个参数的具体含义才可以少走弯路,对于某些特定的需求,比如柱状图与折线图并存时,可不可以用XYPlot,只有试过,根据报错的信息才知道这样做是不可以的。好了,别的感慨不多说了。这里的strimg的内容就是map对象。
自己实现响应mouse事件
上图的map热点信息,大家可以看出,过于简单、表达的方式的不常规很难满足产品的需求,至少要以比较直观的、清晰的方式来表达信息,并且,如果单单只写出X、Y坐标对应的值,相信map热点的存在就是多余,just a Demo.所以为了满足产品级别需求,我们需要自己实现响应mouse事件。
这个解决方案有多种,我是根据自己的理解,将很规则的map数据解析,根据X轴时间取到关联这一天、周、月的详细信息,然后将解析、重组后的数据以变量传到onmouseover事件中。
当然,怎么样让数据与map中各<area>标签的内容一致,以及后续的Tip定位问题,也是需要考虑的。为了方便,这里需要给每个area节点再加一个属性id,并赋值。
StringReader reader = new StringReader(strimg);
InputSource source = new InputSource(reader);
SAXBuilder build = new SAXBuilder();
try {
Document doc = build.build(source);
Element element = doc.getRootElement();
List<Element> list = element.getChildren();
int size1 = list.size();
for (int j = 0; j < size1; j++) {
String nstr = "";
Element e = list.get(j);
Attribute att = e.getAttribute("title");
String value = att.getValue();
value = value.replace("(", "");
value = value.replace(")", "");
Attribute bute = new Attribute("id", j + "");
e.setAttribute(bute);
int id = bute.getIntValue();
int ind = j;
if (j >= size1 / 2) {
ind = j - (size1 / 2);
}
UserFoodRecordBean r = (UserFoodRecordBean) calories
.get(ind);
double bike = r.getBikeCalorie();
double bmrto = r.getBmrTotal();
double sport = r.getSpcal();
double foodcal = r.getCaloriesimple();
double fect=r.getFoodfec();
double cyclomer=r.getCycal();
double total1 = r.getSportTotalCalorie();
double balance = foodcal - total1;
double dailywork = r.getDailyworkcalorie();
String datestr = r.getUserfoodDate();
if (weekormonth == 0) {
datestr = BarChartData.formAtDate(datestr);
} else {
String[] das = datestr.split("-");
int weekindex = Integer.parseInt(das[1]);
if (weekormonth == 1) {
datestr = das[0] + year + index1 + weekindex + week;
} else {
datestr = das[0] + year1 + weekindex + month;
}
}
balance = Double.parseDouble(BarChartData
.formAtCalorie(balance));
nstr += "date=" + datestr + "&BMR=" + bmrto + "&";
if (bike > 0) {
nstr += "e-Bike=" + bike + "&";
}
if (sport > 0) {
nstr += "sport=" + sport + "&";
}
if(cyclomer>0){
nstr+="cyclemer="+cyclomer+"&";
}
if(fect>0){
nstr+="fect="+fect+"&";
}
nstr += "food=" + foodcal + "&" + "total=" + total1
+ "&balance=" + balance + "&dailywork=" + dailywork;
// nstr=HtmlUtils.htmlUnescape(nstr);
att.setValue("");// set nstr to title
String over = "showChartTips('" + nstr + "','" + id
+ "',event);";
String out = "chartlivess();";
Attribute bu = new Attribute("onmouseover", over);
e.setAttribute(bu);
Attribute outatt = new Attribute("onmouseout", out);
e.setAttribute(outatt);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintWriter pw = new PrintWriter(baos); // 用PrintWriter可以解决生成编码为UTF-8格式的XML内容出现中文乱码的问题
Format format = Format.getPrettyFormat();
format.setEncoding("UTF-8");
XMLOutputter output = new XMLOutputter();
output.setFormat(format);
output.output(doc, pw);
String strs = baos.toString();
int j = strs.indexOf("<map");
strs = strs.substring(j);
io.setData("intakemap", strimg, BeanShare.BEAN_SHARE_REQUEST);
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
然后以什么样式展示就与自己的审美有关了……当然可以是美工提供模版,呵
具体的过程就不用累赘了。
看看这样实现后的效果:
另外,在实现的过程中,还有一个问题也是花了许多时间 ,tip的定位问题,要兼容IE6,IE7,IE8,FireFox,并且,出现滚动条时tip可能会错位问题,部分代码如下:
var area= document.getElementById(id); if(area){ var scroleft=document.documentElement.scrollLeft; var left=e.clientX+scroleft; var topy=e.clientY; var top= document.documentElement.scrollTop + topy; left-=66; var _tips=createTip1(); _tips.style.width ="27em"; // _tips.style.height="190px"; _tips.style.display= ""; _tips.style.left = left+"px"; _tips.style.top = top+"px"; _tips.innerHTML=document.getElementById("tipmess").innerHTML; if(navigator.userAgent.indexOf('Firefox') >= 0){ top=window.scrollY + e.clientY; left=window.scrollX+e.clientX; _tips.style.left=left+"px"; _tips.style.top = top+"px"; } }
PS: 部分内容转自http://www.ibm.com/developerworks/cn/java/l-jfreechart/ 作者 刘冬
如果大家之前没有接触过JfreeChart或者是初学者,可以先看看这篇文章 。当初我在实现这个热点map时,感觉这篇文章写的很详细,对我也很有帮助,不过,我实现的具体方案还是经过多次尝试、多次调整,毕竟我这里的要求更高,呵呵