Dojo 导出 Chart 到 PDF文件(Dojo Export Chart To PDF)

分析:在FireFox下进行Dojo Chart绘图,绘图完成后,使用Firebug查看HTML结构,可以看到整个图形Graph有3个部分组成:图形的标题Chart Title(HTML DOM),图形本身Chart(SVG),图形的图标Legend(SVG),上述三个部分组成完整的图形。为了生成完整图形的PDF文件,有必要将三者整合进同一个SVG文件,然后将SVG发送到服务器进行转换。在Chrome下,图形也是由三个部分构成。在IE下,图形有2部分组成,Chart Title 和 Chart 属于同一部分。


思路:将 Chart Title, Chart, Legend 整合进一个 SVG 文件,将 SVG 文件发送到 Server, Server使用Apache Batik将 SVG 文件转换成 PDF 文件,转换完成后返回 PDF 文件。


步骤:

1. 使用Dojo Chart绘图(makeChart);

2. 整合Chart Title, Chart,Legend(integrateChart);

3. 将整合生成的SVG文件发送到服务器,服务器使用Apache Batik将SVG文件转换生成PDF文件(sendSVGToServer);


一、makeChart:

var makeChart = function () {
	chart = new Chart("chartNode", {
		title: "One Year Average Temperature",	// Chart Title
		titleGap: 25,
		titleFont: "normal normal normal 15pt Arial",
		titleFontColor: 'orange',
	});

	// Set the theme
	chart.setTheme(Julie);

	// Add the only/default plot 
	chart.addPlot("default", {
		type: "Lines",
		markers: true
	});
	
	// Add axes
	chart.addAxis("x", { 
		fixLower: "major",  
		title: "Month", 
		titleOrientation: "away",
		labelFunc: function(o) {
			return o;
		},
		htmlLabels: false    // Set this option, make sure x axis is ploted by SVG instead of HTML.
	});
	
	chart.addAxis("y", { 
		vertical: true, 
		title: 'Temperature', 
		titleOrientation: "away", 
		fixLower: "major", 
		fixUpper: "major", 
		minorTickStep: 1 
	});

	// Add tooltip for chart
	new Tooltip(chart, "default", {
		text: function( o ) {
			return 'Month: ' + o.x + '<br />' + 'Temperature: ' + o.y;
		}
	});
	
	
	// Add series
	chart.addSeries('Tokyo', [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]);
	chart.addSeries('New York', [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, 20.1, 14.1, 8.6, 2.5]);
	chart.addSeries('Berlin', [-0.9, 0.6, 3.5, 8.4, 13.5, 17.0, 18.6, 17.9, 14.3, 9.0, 3.9, 1.0]);
	chart.addSeries('London', [3.9, 4.2, 5.7, 8.5, 11.9, 15.2, 17.0, 16.6, 14.2, 10.3, 6.6, 4.8]);

	// Render the chart!
	chart.render();
	
	// Add legend for chart
	legend = new Legend({ chart: chart, horizontal: false }, 'legendNode');
};

二、integrateChart;

var integrateChart = function() {
	var copy, title, titleGroup, legendGroup, shapeContainer, legendSurfaces, lineX, lineY,
	legendTextNodeList, index, jsonStr, obj, shape, x, y, path, newPath, labelText, dateGroup, d;
	
	// make a copy of chart surface
	copy = gfx.createSurface('gfxNode', 800, 400);
	gfxUtils.fromJson(copy, gfxUtils.toJson(chart.surface));

	// Title Group
	if (query('div div', dom.byId('chartNode'))[0]) {
		title = query('div div', dom.byId('chartNode'))[0].textContent;
		titleGroup = copy.createGroup();
		titleGroup.createText({ x: 400, y: 30, text: title, align: 'middle' })
		.setFont({ family: 'Arial', size: '15pt', weight: 'bold'})
		.setFill('orange');
	}

	// Legend Group
	legendGroup = copy.createGroup();
	shapeContainer = copy.createGroup();

	legendSurfaces = legend._surfaces;

	lineX = 600;

	lineY = 20;
	
	index = 0;
	legendTextNodeList = query('.dojoxLegendText');
	
	baseArray.forEach(legendSurfaces, function(surface) {
		gfxUtils.forEach(surface, function(shapes) {
			jsonStr = gfxUtils.toJson(shapes);

			if (jsonStr.indexOf('[') !== 0) {
			
				// Generate a common object corresponding to 'shapes',
				// in case that member of shape cannot be accessed by shape.
				obj = dojo.fromJson(jsonStr);
			
				shape = gfxUtils.fromJson(shapeContainer, jsonStr);
				// console.log('shape.type: ' + shape.shape.type);
				
				if (shape.shape.type === 'line') {
					shape.shape.x1 += lineX;
					shape.shape.y1 += lineY;
					shape.shape.x2 += lineX;
					shape.shape.y2 += lineY;
					legendGroup.createLine({ x1: shape.shape.x1, y1: shape.shape.y1, x2: shape.shape.x2, y2: shape.shape.y2 })
					.setStroke({ color: obj.stroke.color, width: '3' });
				}
				else if (shape.shape.type === 'path') {
					x = lineX + 9;
					y = lineY + 9;
					path = shape.shape.path;
					newPath = path.replace(/^M\s\d+\s\d+m/g, 'M' + ' ' + x + ' ' + y + 'm');
			
					legendGroup.createPath(newPath)
					.setStroke({ color: obj.stroke.color, width: '3' })
					.setFill(obj.stroke.color);
					
					// Get label text
					labelText = legendTextNodeList[index].childNodes[0].nodeValue;

					legendGroup.createText({ x: lineX + 25 , y: lineY + 12, text: labelText, align: 'start' })
					.setFont({ family: 'Arial', size: '10pt' })
					.setFill('black');
					
					lineY += 20;
				}
				
				shapeContainer.clear(true);
			}
		});		// gfxUtils.forEach
		
		index += 1;
	});		// baseArray.forEach

	// Date Group
	dateGroup = copy.createGroup();
	d = new Date();
	dateGroup.createText({ x: 600 , y: 385, text: d.toDateString(), align: 'start' })
	.setFont({ family: 'Arial', size: '10pt' })
	.setFill('black');

};


三、可以使用下列两种方式将svg文件发送到Server-- sendSVGToServer:


方式一:

var sendSVGToServer = function (svg, title) {

	// Cancel last iframe post request
	if (dfd) {
		dfd.cancel();
	}

	// sanitize svg, if doesn't santize svg before post, cannot work in Chrome
	svg = svg.replace(/&nbsp;/g, ' ');
	
	dfd = iframe.post('export.php', {
		preventCache: true,
		data: {
			svg: svg,
			filename: title || 'chart'
		}
	}).then(function(response) {
			console.log('iframe get response');
		},
		
		function(error) {
			if (error.message === 'Request canceled') {
				// console.log('Last iframe post request is canceled');
				return error;
			} else {
				console.error('sendSVGToServer()->iframe.post(): ' + error);
			}
		}
	);
};

方式二:

var sendSVGToServer2 = function(svg, title) {
	
	var formNode = domConstruct.create('form', {
		name: 'svg_submit_form',
		method: 'post',
		action: 'export.php',
		enctype: 'multipart/form-data'
	}, win.body());
	domStyle.set(formNode, 'display', 'none');
	
	// sanitize svg, if doesn't santize svg before post, cannot work in Chrome
	svg = svg.replace(/&nbsp;/g, ' ');
	
	var createInput = function(name, value){
		domConstruct.create('input', {
			type: 'hidden',
			name: name,
			value: value
		}, formNode);
	};
	createInput('svg', svg);
	createInput('filename', title || 'chart');
	
	formNode.submit();

	domConstruct.destroy(formNode);
}

四、服务器端export.php

<?php

if (isset($_POST['svg']) && !empty($_POST['svg'])) {
	getPDF($_POST['svg']);
}

function getPDF($svg) {

	$tempName = md5(rand());
	$outfile = "temp/$tempName.pdf";
	
	$filename = (string) $_POST['filename'];
	if (!$filename) {
		$filename = 'chart';
	}

	define ('BATIK_PATH', '/usr/local/batik/batik-rasterizer.jar');
	
	// generate the temporary file
	if (!file_put_contents("temp/$tempName.svg", $svg)) { 
		die("Couldn't create temporary file. Check that the directory permissions for
			the /temp directory are set to 777.");
	}
	
	// do the conversion
	$output = shell_exec("java -jar ". BATIK_PATH ." -m application/pdf -d $outfile temp/$tempName.svg");
	
	// catch error
	if (!is_file($outfile) || filesize($outfile) < 10) {
		echo "<pre>$output</pre>";
		echo "Error while converting SVG. ";
		
		if (strpos($output, 'SVGConverter.error.while.rasterizing.file') !== false) {
			echo "
			<h4>Debug steps</h4>
			<ol>
			<li>Copy the SVG:<br/><textarea rows=5>" . htmlentities(str_replace('>', ">\n", $svg)) . "</textarea></li>
			<li>Go to <a href='http://validator.w3.org/#validate_by_input' target='_blank'>validator.w3.org/#validate_by_input</a></li>
			<li>Paste the SVG</li>
			<li>Click More Options and select SVG 1.1 for Use Doctype</li>
			<li>Click the Check button</li>
			</ol>";
		}
	} 
	
	// stream it
	else {
		header("Pragma: public");
		header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
		header("Content-Disposition: attachment; filename=\"$filename.pdf\"");
		header("Content-Type: application/pdf");
?>
		<html><body><textarea> <?php echo file_get_contents($outfile); ?> </textarea></body></html>
<?php		
	}
	
	// delete it
	unlink("temp/$tempName.svg");
	unlink($outfile);
}

?>

五、Client 完整测试代码如下,代码仅仅把合并完的单一SVG显示出来,如果服务器端安装了Apache Batik(路径:/usr/local/batik )并部署export.php,只需将下列三行注释去掉就可以生成PDF文件:

Line: 116 // domStyle.set('gfxNode', 'display', 'none');
Line: 201 // doExport(copy, title);
Line: 202 // copy.clear();

<!DOCTYPE HTML>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>Demo: Export Chart To PDF</title>

		<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/dojo/1.8/dijit/themes/claro/claro.css" media="screen">
	</head>
	<body class="claro">
		<div id="container" style="width:800px;">
			<div id="header" style="background-color:#FFA500;clear:both;text-align:center;">
				<h1>Export Chart To PDF</h1>
			</div>
		
			<div id="chartNode" style="width:600px;height:400px;float:left;"></div>
			
			<div id="legendNode" style="width:200px;height:400px;float:left;"></div>
			
			<div id="gfxNode" style="width:800px;height:400px;float:left;"/>
			
			<div id="buttonNode" style="height:25px;float:left;"/>

		</div>
		
		<!-- load dojo and provide config via data attribute -->
		<script src="https://ajax.googleapis.com/ajax/libs/dojo/1.8/dojo/dojo.js" data-dojo-config="isDebug:true, async:true"></script>
		<script>
			
			// Require all dependencies
			require([
					"dijit/form/Button",
					"dijit/registry", 
					"dojox/charting/Chart", 
					"dojox/charting/themes/Julie",  
					"dojox/charting/action2d/Tooltip",
					"dojox/charting/widget/Legend",
					"dojox/charting/plot2d/Lines", 
					"dojox/charting/axis2d/Default", 
					"dojo/request/iframe",
					"dojox/gfx",
					"dojox/gfx/utils",
					"dojo/_base/array",
					"dojo/dom", 
					"dojo/dom-attr",
					"dojo/dom-style",
					"dojo/dom-construct",
					"dojo/query",
					"dojo/domReady!"
					], function(Button, registry, Chart, Julie, Tooltip, Legend, Lines, Default, iframe, gfx, gfxUtils,
						baseArray, dom, domAttr, domStyle, domConstruct, query) {
				var chart, legend, dfd;
				
				var makeChart = function () {
					chart = new Chart('chartNode', {
						title: 'One Year Average Temperature',
						titleGap: 25,
						titleFont: 'normal normal normal 15pt Arial',
						titleFontColor: 'orange',
					});

					// Set the theme
					chart.setTheme(Julie);

					// Add the only/default plot 
					chart.addPlot('default', {
						type: 'Lines',
						markers: true
					});
					
					// Add axes
					chart.addAxis('x', { 
						fixLower: 'major',  
						title: 'Month', 
						titleOrientation: 'away',
						labelFunc: function(o) {
							return o;
						},
						htmlLabels: false
					});
					
					chart.addAxis('y', { 
						vertical: true, 
						title: 'Temperature', 
						titleOrientation: 'away', 
						fixLower: 'major', 
						fixUpper: 'major', 
						minorTickStep: 1 
					});

					// Add tooltip for chart
					new Tooltip(chart, 'default', {
						text: function( o ) {
							return 'Month: ' + o.x + '<br />' + 'Temperature: ' + o.y;
						}
					});
					
					
					// Add series
					chart.addSeries('Tokyo', [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]);
					chart.addSeries('New York', [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, 20.1, 14.1, 8.6, 2.5]);
					chart.addSeries('Berlin', [-0.9, 0.6, 3.5, 8.4, 13.5, 17.0, 18.6, 17.9, 14.3, 9.0, 3.9, 1.0]);
					chart.addSeries('London', [3.9, 4.2, 5.7, 8.5, 11.9, 15.2, 17.0, 16.6, 14.2, 10.3, 6.6, 4.8]);

					// Render the chart!
					chart.render();
					
					// Add legend for chart
					legend = new Legend({ chart: chart, horizontal: false }, 'legendNode');
				};	
				
				
				var integrateChart = function() {
					var copy, title, titleGroup, legendGroup, shapeContainer, legendSurfaces, lineX, lineY,
					legendTextNodeList, index, jsonStr, obj, shape, x, y, path, newPath, labelText, dateGroup, d;
					
					// domStyle.set('gfxNode', 'display', 'none');
					
					// make a copy of chart surface
					copy = gfx.createSurface('gfxNode', 800, 400);
					gfxUtils.fromJson(copy, gfxUtils.toJson(chart.surface));
				
					// Title Group
					if (query('div div', dom.byId('chartNode'))[0]) {
						title = query('div div', dom.byId('chartNode'))[0].textContent;
						titleGroup = copy.createGroup();
						titleGroup.createText({ x: 400, y: 30, text: title, align: 'middle' })
						.setFont({ family: 'Arial', size: '15pt', weight: 'bold'})
						.setFill('orange');
					}

					// Legend Group
					legendGroup = copy.createGroup();
					shapeContainer = copy.createGroup();
				
					legendSurfaces = legend._surfaces;

					lineX = 600;
					// lineY = 200 - legendSurfaces.length * 20;
					lineY = 0;
					
					index = 0;
					legendTextNodeList = query('.dojoxLegendText');
					
					baseArray.forEach(legendSurfaces, function(surface) {
						gfxUtils.forEach(surface, function(shapes) {
							jsonStr = gfxUtils.toJson(shapes);
							// console.log('jsonStr: ' + jsonStr);

							if (jsonStr.indexOf('[') !== 0) {
							
								// Generate a common object corresponding to 'shapes',
								// in case that member of shape cannot be accessed by shape.
								obj = dojo.fromJson(jsonStr);
							
								shape = gfxUtils.fromJson(shapeContainer, jsonStr);
								// console.log('shape.type: ' + shape.shape.type);
								
								if (shape.shape.type === 'line') {
									shape.shape.x1 += lineX;
									shape.shape.y1 += lineY;
									shape.shape.x2 += lineX;
									shape.shape.y2 += lineY;
									legendGroup.createLine({ x1: shape.shape.x1, y1: shape.shape.y1, x2: shape.shape.x2, y2: shape.shape.y2 })
									.setStroke({ color: obj.stroke.color, width: '3' });
								}
								else if (shape.shape.type === 'path') {
									x = lineX + 9;
									y = lineY + 9;
									path = shape.shape.path;
									newPath = path.replace(/^M\s\d+\s\d+m/g, 'M' + ' ' + x + ' ' + y + 'm');
							
									legendGroup.createPath(newPath)
									.setStroke({ color: obj.stroke.color, width: '3' })
									.setFill(obj.stroke.color);
									
									// Get label text
									labelText = legendTextNodeList[index].childNodes[0].nodeValue;

									console.log('labelText: ' + labelText);
									legendGroup.createText({ x: lineX + 25 , y: lineY + 12, text: labelText, align: 'start' })
									.setFont({ family: 'Arial', size: '10pt' })
									.setFill('black');
									
									lineY += 20;
								}
								
								shapeContainer.clear(true);
							}
						});		// gfxUtils.forEach
						
						index += 1;
					});		// baseArray.forEach
				
					// Date Group
					dateGroup = copy.createGroup();
					d = new Date();
					dateGroup.createText({ x: 600 , y: 385, text: d.toDateString(), align: 'start' })
					.setFont({ family: 'Arial', size: '10pt' })
					.setFill('black');
				
					// doExport(copy, title);
					// copy.clear();
				};

				var doExport = function(surface, title) {
					gfxUtils.toSvg(surface).then(
						function(svg) {
							sendSVGToServer(svg, title);		
						},
						function(err) {
							console.err('gfxUtils.toSvg: ' + err);
						}
					);
				};
				
				var sendSVGToServer = function(svg, title) {
					// Cancel last iframe post request
					if (dfd) {
						dfd.cancel();
					}

					// sanitize svg, if doesn't santize svg before post, cannot work in Chrome
					svg = svg.replace(/&nbsp;/g, ' ');
					
					dfd = iframe.post('export.php', {
						preventCache: true,
						data: {
							svg: svg,
							filename: title || 'chart'
						}
					}).then(function(response) {
							console.log('sendSVGToServer()->iframe.post(): get response');
						},
						
						function(error) {
							if (error.message === 'Request canceled') {
								// console.log('iframe post request is canceled');
								return error;
							} else {
								console.error('sendSVGToServer()->iframe.post(): ' + error);
							}
						}
					);
				};
				
				var makeButton = function() {
					var button = new Button({
						label: 'Export to PDF',
						onClick: function() {
							integrateChart()
						}
					}, 'buttonNode');
		
					button.startup();
				}
				
				makeChart();
				makeButton();
			});
			
		</script>
	</body>
</html>




评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值