做系统的时候都要配合报表展示一些图表,例如柱状图、饼图还有曲线图,以前是用VML做成通用的,数值的计算和VML在页面上的显示,全部是用javascript做的,可是VML中的文字定位不是特别舒服,javascript计算数字也不是很精确。最近学了几天SVG,觉得不错,所以就想用JAVA生成SVG文件,然后显示,网上没有类似的文章,所以我就把自己的第一个作品放出来,希望大家能够提点向我提电意见,改进。我以后会陆续放做好的饼图、曲线图。我相信这是国内第一个类似的程序,希望能够起到抛砖引玉的作用。
这个程序能够生成单柱柱状图,也能生成多柱柱状图。我的注释写得相当的详细,相信大家都能看的懂。我没有使用APACHE的Batik来生成SVG文件,而是先组成字符串然后生成SVG文件。以下是源码,我做的时候是按照12组数据设计的,当数据少的话,柱子会很宽,我正在改进。
package com.hhh.Paint;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigDecimal;
/**
* 该类是使用SVG画动态柱状图,只需调用本类的静态方法createSVG(String fileRealPath,String[]
* pData,String[] pDataName)即可,我没有使用APACHE提供的SVG类库来生成SVG文件,只是将所有SVG描述用字符串返回
*
* @author 顾国勇
* @version 1.0
*/
public class PaintHistogram {
/**
* 正值所用的12种颜色
*/
public static String[][] color = { { "#ff2222", "#ffaaaa", "#aa1111" },
{ "#d0b83a", "#f2e692", "#9f8703" },
{ "#ffdf00", "#fef195", "#ce9a31" },
{ "#22FF22", "#aaffaa", "green" },
{ "#ff9e00", "#ffbe08", "#bd7500" },
{ "#799AE1", "#9aabEe", "#5778a1" },
{ "#99c741", "#aef292", "#3e941b" },
{ "#d0b83a", "#f2e692", "#9f8703" },
{ "#319a00", "#66cc00", "#297110" },
{ "#c27f34", "#d6a97b", "#82522b" },
{ "#2222ff", "#aaaaff", "#1111aa" },
{ "#ff2222", "#ffaaaa", "#aa1111" } }; //,{"#99c741","#aef292","#3e941b"}
/**
* 负值所用的颜色
*/
public static String[][] colorNeg = { { "black", "#4F4F4F", "#757575" },
{ "#1A0B0F", "#4F4F4F", "#757575" } };
/**
* 存放转换成double型的数据,用于单例柱形
*/
public static double[] data;
/**
* 存放转换成double型的数据,用于多例柱形
*/
public static double[][] data1;
/**
* 数据总数,默认值为0
*/
public static int dataNum = 0;
/**
* 默认柱状图X轴的起始X值
*/
public static double HistogramXSx = 50.0;
/**
* 默认柱状图X轴的Y值
*/
public static double HistogramXy = 440.0;
/**
* 将元数据从String转换成double,用于单例柱形
*
* @param data
* 字符串数组形式的元数据
* @return 转换好的数据
*/
static double[] convertDataToDouble(String[] data) {
double[] dData = new double[data.length];
dataNum = 0;
for (int i = 0; i < data.length; i++) {
if (data[i] != null && !data[i].equalsIgnoreCase("")
&& !data[i].equalsIgnoreCase(" ")) {
dData[i] = Double.valueOf(data[i]).doubleValue();
dataNum += 1;
} else {
dData[i] = new Double(0.0).doubleValue();
dataNum += 1;
}
}
return dData;
}
/**
* 将元数据从String转换成double,用于多例柱形
*
* @param data
* 字符串数组形式的元数据
* @return 转换好的数据
*/
static double[][] convertDataToDouble(String[][] data) {
double[][] dData = new double[data.length][data[0].length];
dataNum = 0;
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
if (data[i][j] != null && !data[i][j].equalsIgnoreCase("")
&& !data[i][j].equalsIgnoreCase(" ")) {
dData[i][j] = Double.parseDouble(data[i][j]);//Double.valueOf(data[i][j]).doubleValue();
dataNum += 1;
} else {
dData[i][j] = new Double(0.0).doubleValue();
dataNum += 1;
}
}
}
return dData;
}
/**
* @param fileRealPath
* 指定的SVG文件的全路径,用于单例柱形
* @param pData
* 元数据数组
* @param pDataName
* 元数据数组名称
*/
public static void createSVG(String fileRealPath, String[] pData,
String[] pDataName) throws Exception {
String sFile = paint(pData, pDataName);
try {
byte[] byteFil = sFile.getBytes("UTF-8");
File svgFile = new File(fileRealPath);
if (svgFile.exists()) {
svgFile.delete();
}
FileOutputStream fos = new FileOutputStream(svgFile);
fos.write(byteFil);
fos.close();
} catch (Exception ex) {
System.out.print(ex.getMessage());
}
}
/**
* @param fileRealPath
* 指定的SVG文件的全路径,用于多例柱形
* @param pData
* 元数据数组
* @param pDataName
* 元数据数组名称
*/
public static void createSVG(String fileRealPath, String[][] pData,
String[] pDataName) {
try {
String sFile = paint(pData, pDataName);
byte[] byteFil = sFile.getBytes("UTF-8");
File svgFile = new File(fileRealPath);
if (svgFile.exists()) {
svgFile.delete();
}
FileOutputStream fos = new FileOutputStream(svgFile);
fos.write(byteFil);
fos.close();
} catch (Exception ex) {
System.out.print("createSVG:" + ex.getMessage());
}
}
/**
* 根据原始数据过滤出最大最小值,已经考虑了数据正负的各种情况(全是正数,全是负数,有正有负),如果元数据中最大值或最小值的绝对值是小于1的,那最后返回的相应值的绝对值是1;
* 如果最大或最小的绝对值小于100,那最后返回的相应值的绝对值是10的倍数;如果最大或最小的绝对值大于100,那最后返回的相应值的绝对值是50的倍数
*
* @param data
* <B>原始数据 </B>
* @return <B>包含绝对值最大的值 </B>
*/
static double getValueMax(double[] pData) {
//存放所有数据值
double max = pData[0];
double min = pData[0];
for (int i = 0; i < pData.length; i++) {
if (pData[i] > max)
max = pData[i];
if (pData[i] < min)
min = pData[i];
}
//全是负的
if (max <= 0 && min < 0) {
min = Math.floor(min);
double tMin = Math.abs(min);
max = 0;
if (tMin <= 1)
min = -1;
else {
min = -(tMin < 100 ? Math.ceil(tMin / 10) * 10 : Math
.ceil(tMin / 50) * 50);
}
}
//有正有负
if (min < 0 && max > 0) {
max = Math.ceil(max);
if (max <= 1)
max = 1;
else {
max = max < 100 ? Math.ceil(max / 10) * 10 : Math
.ceil(max / 50) * 50;
}
min = Math.abs(Math.floor(min));
if (min <= 1)
min = -1;
else {
min = -(min < 100 ? Math.ceil(min / 10) * 10 : Math
.ceil(min / 50) * 50);
}
}
//全是正的
if (min >= 0 && max >= 0) {
if (max == 0.0) {
max = min = 0.0;
} else {
if (max <= 1)
max = 1;
else {
max = max < 100 ? Math.ceil(max / 10) * 10 : Math
.ceil(max / 50) * 50;
}
}
}
double absMax = Math.abs(max) > Math.abs(min) ? Math.abs(max) : Math
.abs(min);
return absMax;
}
/**
* 类初始化,主要是声明了文件开头和滤镜,对于画正负Y轴都通用
*
* @return SVG文件头
*/
static String initialize() {
//文件头声明
StringBuffer sFile = new StringBuffer();
sFile.append("<?xml version='1.0' encoding='UTF-8'?>");
sFile.append("\n");
sFile
.append("<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.0//EN' 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'>");
sFile.append("\n");
sFile
.append("<svg width='700' height='500' viewBox='0 0 700 500' xmlns='http://www.w3.org/2000/svg'>");
sFile.append("\n");
sFile.append("<desc>Histogram</desc>");
sFile.append("\n");
sFile
.append("<rect x='0' y='0' width='700' height='500' fill='none' stroke='blue' stroke-width='1'/>");
sFile.append("\n");
//定义滤镜和箭头
sFile.append("<defs>");
sFile.append("\n");
sFile
.append(" <marker id='Triangle' viewBox='0 0 10 10' refX='0' refY='5' markerUnits='strokeWidth' markerWidth='4' markerHeight='10' orient='auto'>");
sFile.append("\n");
sFile.append(" <path d='M 0 0 L 10 5 L 0 10 z'/>");
sFile.append("\n");
sFile.append(" </marker>");
sFile.append("\n");
sFile.append(" <linearGradient id='linear1'>");
sFile.append("\n");
sFile.append(" <stop offset='0%' stop-color='red'/>");
sFile.append("\n");
sFile.append(" <stop offset='30%' stop-color='white'/>");
sFile.append("\n");
sFile.append(" <stop offset='100%' stop-color='blue'/>");
sFile.append("\n");
sFile.append(" </linearGradient>");
sFile.append("\n");
sFile
.append(" <filter id='MyFilter' filterUnits='userSpaceOnUse' x='0' y='0' width='100%' height='100%'>");
sFile.append("\n");
sFile
.append(" <feGaussianBlur in='SourceAlpha' stdDeviation='3' result='blur'/>");
sFile.append("\n");
sFile
.append(" <feOffset in='blur' dx='4' dy='4' result='offsetBlur'/>");
sFile.append("\n");
sFile.append("\n");
sFile
.append(" <feSpecularLighting in='blur' surfaceScale='.5' specularConstant='2.5' specularExponent='128' lighting-color='#bbbbbb' result='specOut'>");
sFile.append("\n");
sFile.append(" <fePointLight x='-600' y='-100' z='2200'/>");
sFile.append("\n");
sFile.append(" </feSpecularLighting>");
sFile.append("\n");
sFile
.append(" <feComposite in='specOut' in2='SourceAlpha' operator='in' result='specOut'/>");
sFile.append("\n");
sFile
.append(" <feComposite in='SourceGraphic' in2='specOut' operator='arithmetic' k1='0' k2='1' k3='1' k4='0' result='litPaint'/>");
sFile.append("\n");
sFile.append(" <feMerge>");
sFile.append("\n");
sFile.append(" <feMergeNode in='offsetBlur'/>");
sFile.append("\n");
sFile.append(" <feMergeNode in='litPaint'/>");
sFile.append("\n");
sFile.append(" </feMerge>");
sFile.append("\n");
sFile.append(" </filter>");
sFile.append("\n");
sFile.append("<script type=\"text/javascript\">");
sFile.append("\n");
sFile.append("<![CDATA[");
sFile.append("\n");
sFile.append(" function showValue(value) {");
sFile.append("\n");
sFile.append(" alert(value);");
sFile.append("\n");
sFile.append(" }");
sFile.append("\n");
sFile.append("]]>");
sFile.append("\n");
sFile.append("</script>");
sFile.append("\n");
sFile.append("</defs>");
sFile.append("\n");
//X轴
sFile
.append("<line x1='50' y1='440' x2='660' y2='440' stroke-width='2' stroke-opacity='.4' fill='blue' stroke='#333300' />");
sFile.append("\n");
//Y轴
sFile
.append("<line x1='50' y1='440' x2='50' y2='40' stroke-width='2' stroke-opacity='.4' fill='blue' stroke='#333300' />");
sFile.append("\n");
sFile
.append("<line x1='60' y1='30' x2='60' y2='430' stroke-width='0.5' stroke='#333300'/>");
/* Y轴刻度线 */
for (int i = 0; i < 11; i++) {
double tempY1 = 40 + i * 40.0; //每根Y轴刻度线的起始位置
sFile.append("<line x1='50' y1='" + tempY1 + "' x2='60' y2='"
+ (tempY1 - 10) + "' stroke-width='1' stroke='#333300'/>");
sFile.append("<line x1='60' y1='" + (tempY1 - 10)
+ "' x2='670' y2='" + (tempY1 - 10)
+ "' stroke='#333300'/>");
sFile.append("\n");
// sFile.append("<polygon points='50," + tempY1 + " 660," + tempY1
// + " 670," + (tempY1 - 10) + " 60," + (tempY1 - 10)
// + " ' ");
// sFile
// .append("
// style='fill:rgb(37,170,219);stroke:rgb(37,170,219);stroke-width:1;fill-opacity:0.3;stroke-opacity:0;opacity:0.8'/>");
// sFile.append("\n");
}
return sFile.toString();
}
/**
* @param args
*/
public static void main(String[] args) {
try {
String[][] data = { { "54", "44", "122" }, { "", "12", "-468" },
{ "78", "520", "10" } };
String[] data1 = { "54", "4424.225", "12200" ,"-4658" };
String[] dataName = { "营业所一", "二二二二", "III" };
PaintHistogram.createSVG("d:\\t1.svg", data, dataName);
PaintHistogram.createSVG("d:\\t2.svg", data1, dataName);
} catch (Exception ex) {
System.out.print(ex.getMessage());
}
}
/**
* 画出柱状及刻度等,适用于X轴每格都只显示一个柱型
*
* @param pData
* 元数据
* @param pDataName
* 元数据的名称
*/
static String paint(String[] pData, String[] pDataName) throws Exception {
StringBuffer sFile = new StringBuffer();
data = convertDataToDouble(pData);
double valueMM = getValueMax(data); //取得最大值
sFile.append(initialize());
//X Y步长
double yStep = 400.0 / valueMM;
double xStep = (new BigDecimal(600 / pDataName.length).setScale(3,
BigDecimal.ROUND_HALF_UP)).doubleValue();
double colWidth = xStep / 2; //柱型的宽度
double colWidthPre = xStep / 4; //每个单元格起始与柱型的距离
double colWidthNext = xStep / 4; //每个单元格结束与柱型的距离
/* x轴刻度线 */
for (int i = 0; i < pDataName.length; i++) {
double tempX1 = i == 0 ? HistogramXSx + i * xStep : HistogramXSx
+ i * xStep + 10; //每根X轴刻度线的起始位置
//double tempX2 = tempX1 + colWidthPre; //每根柱状的起始位置
sFile.append("<line x1='" + tempX1 + "' y1='" + HistogramXy
+ "' x2='" + (tempX1 + 10.0) + "' y2='"
+ (HistogramXy - 10)
+ "' stroke-width='1' stroke='#333300'/>");
sFile.append("\n");
}
/* 画Y轴刻度值 */
int maxValue = (int) valueMM;
int valueX; //Y轴刻度值起始的X坐标
String fontsize = "";
if (maxValue > 10000) {
valueX = 5;
fontsize = "11px";
} else {
valueX = 15;
fontsize = "13px";
}
for (int i = 0; i < 10; i++) {
sFile.append("<text x='" + valueX + "' y='" + (45 + i * 40)
+ "' fill='black' font-family='Verdana' font-size='"
+ fontsize + "'>");
sFile.append("\n");
sFile.append((maxValue / 10) * (10 - i));
sFile.append("\n");
sFile.append("</text>");
sFile.append("\n");
}
/* 画矩形 */
sFile.append("<g style='filter:url(#MyFilter)'>");
sFile.append("\n");
for (int i = 0; i < pDataName.length; i++) {
double tempX1 = i == 0 ? HistogramXSx + i * xStep : HistogramXSx
+ i * xStep + 10; //每根X轴刻度线的起始位置
double tempX2 = tempX1 + colWidthPre; //每根柱状的起始位置
double colHeight = Math.ceil(Math.abs(data[i]) * yStep);
double tempY = 440 - colHeight;
String[] colorHistogram; //三个柱面的颜色
if (data[i] < 0) {
colorHistogram = colorNeg[0];
sFile.append(paintHistogram(colWidth, colHeight, tempX2, tempY,
colorHistogram, data[i]));
} else if (data[i] > 0) {
int n = i % color.length;
colorHistogram = color[n];
sFile.append(paintHistogram(colWidth, colHeight, tempX2, tempY,
colorHistogram, data[i]));
}
/* 添加X轴字段说明 字无法在一行内显示的话,需要换行 */
int n = (int) xStep / 13;//每行显示几个字符
int m = pDataName[i].length() % n == 0 ? pDataName[i].length() / n
: (int) (pDataName[i].length() / n + 1);//总共显示几行
int l = 0;//记录总共处理了多少字符
if (m > 1) {
sFile
.append("<text fill='blue' font-size='15px' font-family='STFangsong'>");
for (int j = 0; j < m; j++) {
for (int z = 0; z < n; z++) {
l = j * n + z;
if (l == pDataName[i].length()) {
break;
}
sFile.append("<tspan x='" + (tempX1 + z * 15) + "' y='"
+ (460 + j * 20) + "' >");
sFile.append(pDataName[i].substring(l, l + 1));
sFile.append("</tspan>");
}
if (l == pDataName[i].length()) {
break;
}
}
sFile.append("</text>");
sFile.append("\n");
} else {
sFile
.append("<text x='"
+ (5 + tempX1)
+ "' y='460' fill='blue' font-size='15px' font-family='STFangsong'>"
+ pDataName[i] + "</text>");
sFile.append("\n");
}
}
sFile.append("</g>");
sFile.append("\n");
sFile.append("</svg>");
return sFile.toString();
}
/**
* 画出柱状及刻度等,适用于X轴每格都显示多个柱型
*
* @param pData
* 具体的元数据
* @param pDataName
* 元数据的名称
*/
static String paint(String[][] pData, String[] pDataName) {
StringBuffer sFile = new StringBuffer();
data1 = convertDataToDouble(pData);
//取得最大值
double[] tempValue = new double[dataNum];
for (int i = 0; i < data1.length; i++) {
for (int j = 0; j < data1[i].length; j++) {
tempValue[i * data1[i].length + j] = data1[i][j];
}
}
double valueMM = getValueMax(tempValue);
sFile.append(initialize());
double yStep = 400.0 / valueMM;
double xStep = (new BigDecimal(600 / pDataName.length).setScale(3,
BigDecimal.ROUND_HALF_UP)).doubleValue();
double colWidthSum = xStep / 2; //多个柱型的宽度之和
double colWidthPre = xStep / 4; //每个单元格起始与柱形的距离
double colWidthNext = xStep / 4; //每个单元格结束与柱形的距离
double colWidth = colWidthSum / data1[0].length; //单个柱形的宽度
/* x轴刻度线 */
for (int i = 0; i < pDataName.length; i++) {
double tempX1 = i == 0 ? HistogramXSx + i * xStep : HistogramXSx
+ i * xStep + 10; //每根X轴刻度线的起始位置
//double tempX2 = tempX1 + colWidthPre; //每根柱形的起始位置
sFile.append("<line x1='" + tempX1 + "' y1='" + HistogramXy
+ "' x2='" + (tempX1 + 10.0) + "' y2='"
+ (HistogramXy - 10)
+ "' stroke-width='1' stroke='#333300'/>");
sFile.append("\n");
}
/* 画Y轴刻度值 */
int maxValue = (int) valueMM;
int valueX;//Y轴刻度值起始的X坐标
String fontsize = "";
if (maxValue > 10000) {
valueX = 5;
fontsize = "11px";
} else {
valueX = 15;
fontsize = "13px";
}
for (int i = 0; i < 10; i++) {
sFile.append("<text x='" + valueX + "' y='" + (45 + i * 40)
+ "' fill='black' font-family='Verdana' font-size='"
+ fontsize + "'>");
sFile.append("\n");
sFile.append((maxValue / 10) * (10 - i));
sFile.append("\n");
sFile.append("</text>");
sFile.append("\n");
}
/* 画矩形 */
sFile.append("<g style='filter:url(#MyFilter)'>");
sFile.append("\n");
for (int i = 0; i < pDataName.length; i++) {
//每根X轴刻度线的起始位置
double tempX1 = i == 0 ? HistogramXSx + i * xStep : HistogramXSx
+ i * xStep + 10;
for (int j = 0; j < data1[i].length; j++) {
double tempX2 = tempX1 + colWidthPre + j * colWidth; //每根柱形的起始位置
double colHeight = Math.ceil(Math.abs(data1[i][j]) * yStep);
double tempY = 440 - colHeight;
String[] colorHistogram; //三个柱面的颜色
if (data1[i][j] < 0) {
colorHistogram = colorNeg[0];
sFile.append(paintHistogram(colWidth, colHeight, tempX2,
tempY, colorHistogram, data1[i][j]));
} else if (data1[i][j] > 0) {
int n = j % color.length;
colorHistogram = color[n];
sFile.append(paintHistogram(colWidth, colHeight, tempX2,
tempY, colorHistogram, data1[i][j]));
}
}
/* 添加X轴字段说明 字无法在一行内显示的话,需要换行 */
int n = (int) xStep / 13;//每行显示几个字符
int m = pDataName[i].length() % n == 0 ? pDataName[i].length() / n
: (int) (pDataName[i].length() / n + 1);//总共显示几行
int l = 0;//记录总共处理了多少字符
if (m > 1) {
sFile
.append("<text fill='blue' font-size='15px' font-family='STFangsong'>");
for (int j = 0; j < m; j++) {
for (int z = 0; z < n; z++) {
l = j * n + z;
if (l == pDataName[i].length()) {
break;
}
sFile.append("<tspan x='" + (tempX1 + z * 15) + "' y='"
+ (460 + j * 20) + "' >");
sFile.append(pDataName[i].substring(l, l + 1));
sFile.append("</tspan>");
}
if (l == pDataName[i].length()) {
break;
}
}
sFile.append("</text>");
sFile.append("\n");
} else {
sFile
.append("<text x='"
+ (5 + tempX1)
+ "' y='460' fill='blue' font-size='15px' font-family='STFangsong'>"
+ pDataName[i] + "</text>");
sFile.append("\n");
}
}
sFile.append("</g>");
sFile.append("\n");
sFile.append("</svg>");
return sFile.toString();
}
/**
* 画单一柱形
*
* @param colWidth
* 柱形宽度
* @param colHeight
* 柱形高度
* @param x
* 柱形X值
* @param y
* 柱形Y值
* @param color
* 柱形三个面的颜色
* @param dataValue
* 柱形代表的值
* @return SVG字符串
*/
static String paintHistogram(double colWidth, double colHeight, double x,
double y, String[] color, double dataValue) {
StringBuffer sFile = new StringBuffer();
double dOffset = (colWidth / 3) > 10.0 ? 10.0 : (colWidth / 3);//斜面的偏移量
sFile.append("<rect width='" + colWidth + "' height='" + colHeight
+ "' x='" + x + "' y='" + y + "' fill='" + color[0]
+ "' onclick='showValue(" + dataValue + ")'/>");
sFile.append("\n");
sFile.append("<polygon points='" + x + "," + y + " " + (x + dOffset)
+ "," + (y - dOffset) + " " + (x + dOffset + colWidth) + ","
+ (y - dOffset) + " " + (x + colWidth) + "," + y
+ "' style='fill:" + color[1] + ";stroke:" + color[1]
+ ";stroke-width:1;' onclick='showValue(" + dataValue + ")'/>");
sFile.append("\n");
sFile.append("<polygon points='" + (x + dOffset + colWidth) + ","
+ (y - dOffset) + " " + (x + colWidth) + "," + y + " "
+ (x + colWidth) + "," + 440 + " " + (x + dOffset + colWidth)
+ "," + (y - dOffset + colHeight) + "' style='fill:" + color[2]
+ ";stroke:" + color[2]
+ ";stroke-width:1;' onclick='showValue(" + dataValue + ")'/>");
return sFile.toString();
}
}