跳转到内容

User:A2569875/Module:Graph/Chart2D.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Module:Graph/Chart2D.js
( function () {
	'use strict';

	window.mw = window.mw || {};
	mw.libs = mw.libs || {};
	var GW = mw.libs.GraphWidget = mw.libs.GraphWidget || {};
	GW.Renderers = GW.Renderers || {};

	// 將 API 平面紀錄集整理為多數列結構 (對齊 DataProcess.wrangleRecordsToArgs)
	function pivotRecords( spec, rows ) {
		var xField = spec.xField || 'date';
		var yField = spec.yField || 'views';
		var sizeField = spec.sizeField;
		var groupField = spec.groupField;
		var first = rows[ 0 ] || {};

		if ( first[ xField ] === undefined ) {
			if ( first._xfield !== undefined ) {
				xField = '_xfield';
			} else if ( first.date !== undefined ) {
				xField = 'date';
			} else if ( first.timestamp !== undefined ) {
				xField = 'timestamp';
			} else if ( first.x !== undefined ) {
				xField = 'x';
			}
		}
		if ( first[ yField ] === undefined ) {
			if ( first._yfield !== undefined ) {
				yField = '_yfield';
			} else if ( first.views !== undefined ) {
				yField = 'views';
			} else if ( first.value !== undefined ) {
				yField = 'value';
			} else if ( first.y !== undefined ) {
				yField = 'y';
			}
		}

		var rawType = String( spec.type || 'line' ).toLowerCase();
		var isStacked = rawType.indexOf( 'stacked' ) === 0;
		var baseType = isStacked ? rawType.slice( 7 ) : rawType;
		if ( baseType === 'bar' || baseType === 'groupedbar' || baseType === 'groupedrect' || baseType === 'hist' || baseType === 'histogram' ) {
			baseType = 'rect';
		}

		var isDateX = spec.xType === 'date' || xField === 'date' || xField === 'timestamp' || xField === '_xfield';
		var isOrdinalX = ( baseType === 'rect' && rawType.indexOf( 'hist' ) === -1 ) || spec.xType === 'string';

		var xCats = [];
		var xCatMap = {};
		var seriesMap = {};
		var seriesList = [];

		for ( var i = 0; i < rows.length; i++ ) {
			var r = rows[ i ];
			var rawX = r[ xField ];
			var rawY = r[ yField ];
			var yv = Number( rawY );
			if ( rawX === undefined || rawX === null || isNaN( yv ) ) {
				continue;
			}
			var xv;
			if ( isOrdinalX ) {
				var xKey = String( rawX );
				if ( xCatMap[ xKey ] === undefined ) {
					xCatMap[ xKey ] = xCats.length;
					xCats.push( xKey );
				}
				xv = xCatMap[ xKey ];
			} else if ( isDateX ) {
				xv = GW.Utils.parseDateMs( rawX );
				if ( isNaN( xv ) ) {
					isDateX = false;
					isOrdinalX = true;
					var sKey = String( rawX );
					if ( xCatMap[ sKey ] === undefined ) {
						xCatMap[ sKey ] = xCats.length;
						xCats.push( sKey );
					}
					xv = xCatMap[ sKey ];
				}
			} else {
				xv = Number( rawX );
				if ( isNaN( xv ) ) {
					var dMs = GW.Utils.parseDateMs( rawX );
					if ( !isNaN( dMs ) ) {
						isDateX = true;
						xv = dMs;
					} else {
						isOrdinalX = true;
						var kStr = String( rawX );
						if ( xCatMap[ kStr ] === undefined ) {
							xCatMap[ kStr ] = xCats.length;
							xCats.push( kStr );
						}
						xv = xCatMap[ kStr ];
					}
				}
			}

			var gKey = ( groupField && r[ groupField ] !== undefined ) ? String( r[ groupField ] ) : ( spec.yAxisTitle || yField || 'y1' );
			if ( !seriesMap[ gKey ] ) {
				seriesMap[ gKey ] = { title: gKey, pts: [], byX: {} };
				seriesList.push( seriesMap[ gKey ] );
			}
			var sv = sizeField && r[ sizeField ] !== undefined ? Number( r[ sizeField ] ) : null;
			var ptObj = { x: xv, y: yv, y0: 0, y1: yv, rawX: rawX, size: sv };
			seriesMap[ gKey ].pts.push( ptObj );
			seriesMap[ gKey ].byX[ xv ] = ptObj;
		}

		if ( isStacked && seriesList.length > 1 ) {
			var allXKeys = [];
			var seenX = {};
			for ( var s = 0; s < seriesList.length; s++ ) {
				for ( var p = 0; p < seriesList[ s ].pts.length; p++ ) {
					var xk = seriesList[ s ].pts[ p ].x;
					if ( !seenX[ xk ] ) {
						seenX[ xk ] = true;
						allXKeys.push( xk );
					}
				}
			}
			allXKeys.sort( function ( a, b ) { return a - b; } );
			for ( var xi = 0; xi < allXKeys.length; xi++ ) {
				var curX = allXKeys[ xi ];
				var posSum = 0, negSum = 0;
				for ( var si = 0; si < seriesList.length; si++ ) {
					var pt = seriesList[ si ].byX[ curX ];
					if ( pt ) {
						if ( pt.y >= 0 ) {
							pt.y0 = posSum;
							posSum += pt.y;
							pt.y1 = posSum;
						} else {
							pt.y0 = negSum;
							negSum += pt.y;
							pt.y1 = negSum;
						}
					}
				}
			}
		}

		return {
			baseType: baseType,
			isStacked: isStacked,
			isDateX: isDateX,
			isOrdinalX: isOrdinalX,
			xCats: xCats,
			seriesList: seriesList
		};
	}

	// 主直角座標系渲染器 (支援折線、面積、堆疊、長條、散佈與氣泡圖)
	function renderCartesian2D( container, spec, rows, meta ) {
		var U = GW.Utils;
		if ( !U || !Array.isArray( rows ) || rows.length === 0 ) {
			return false;
		}

		var pv = pivotRecords( spec, rows );
		var seriesList = pv.seriesList;
		if ( seriesList.length === 0 ) {
			return false;
		}

		var plotW = Math.max( 100, Number( spec.width ) || 450 );
		var plotH = Math.max( 70, Number( spec.height ) || 200 );
		var grey = '#54595d';
		var baseType = pv.baseType;
		var isStacked = pv.isStacked;
		var isDateX = pv.isDateX;
		var isOrdinalX = pv.isOrdinalX;
		var xCats = pv.xCats;

		var dataXMin = Infinity, dataXMax = -Infinity;
		var dataYMin = ( isStacked || baseType === 'rect' || baseType === 'area' ) ? 0 : Infinity;
		var dataYMax = -Infinity;
		var sizeMin = Infinity, sizeMax = -Infinity;

		for ( var si = 0; si < seriesList.length; si++ ) {
			var pts = seriesList[ si ].pts;
			for ( var pi = 0; pi < pts.length; pi++ ) {
				var pt = pts[ pi ];
				if ( pt.x < dataXMin ) {
					dataXMin = pt.x;
				}
				if ( pt.x > dataXMax ) {
					dataXMax = pt.x;
				}
				if ( pt.y0 < dataYMin ) {
					dataYMin = pt.y0;
				}
				if ( pt.y1 < dataYMin ) {
					dataYMin = pt.y1;
				}
				if ( pt.y0 > dataYMax ) {
					dataYMax = pt.y0;
				}
				if ( pt.y1 > dataYMax ) {
					dataYMax = pt.y1;
				}
				if ( pt.size !== null && !isNaN( pt.size ) ) {
					if ( pt.size < sizeMin ) {
						sizeMin = pt.size;
					}
					if ( pt.size > sizeMax ) {
						sizeMax = pt.size;
					}
				}
			}
		}

		if ( dataYMin === Infinity ) {
			dataYMin = 0;
			dataYMax = 10;
		}
		if ( spec.yScaleType === 'log' && dataYMin <= 0 ) {
			dataYMin = 1;
		}

		var reqYMin = spec.yMin !== undefined && spec.yMin !== null ? Number( spec.yMin ) : dataYMin;
		var reqYMax = spec.yMax !== undefined && spec.yMax !== null ? Number( spec.yMax ) : dataYMax;
		var yTickObj = U.computeNiceTicks( reqYMin, reqYMax, Math.max( 3, Math.floor( plotH / 36 ) ), spec.yScaleType, spec.yType === 'integer' );
		var yMin = ( spec.yMin !== undefined && spec.yMin !== null ) ? Number( spec.yMin ) : yTickObj.min;
		var yMax = ( spec.yMax !== undefined && spec.yMax !== null ) ? Number( spec.yMax ) : yTickObj.max;
		if ( yMax <= yMin ) {
			yMax = yMin + 1;
		}

		var xMin = dataXMin, xMax = dataXMax;
		var xTicks = [];
		if ( !isOrdinalX ) {
			if ( isDateX ) {
				if ( xMax <= xMin ) {
					xMax = xMin + 86400000;
				}
				var nDateTicks = Math.max( 3, Math.min( 7, Math.floor( plotW / 85 ) ) );
				for ( var dti = 0; dti <= nDateTicks; dti++ ) {
					xTicks.push( xMin + ( dti / nDateTicks ) * ( xMax - xMin ) );
				}
			} else {
				var xTickObj = U.computeNiceTicks( xMin, xMax, Math.max( 3, Math.floor( plotW / 65 ) ), spec.xScaleType, spec.xType === 'integer' );
				xMin = xTickObj.min;
				xMax = xTickObj.max;
				xTicks = xTickObj.ticks;
			}
		}

		var maxYLblLen = 3;
		for ( var yi = 0; yi < yTickObj.ticks.length; yi++ ) {
			var sLbl = U.formatNumber( yTickObj.ticks[ yi ], spec.yAxisFormat, spec.yType === 'integer' );
			if ( sLbl.length > maxYLblLen ) {
				maxYLblLen = sLbl.length;
			}
		}

		var showLegend = spec.legend !== undefined && spec.legend !== null && spec.legend !== false && spec.legend !== 'false' && spec.legend !== '0';
		var legW = 0;
		if ( showLegend && seriesList.length > 0 ) {
			legW = 80;
			for ( var li = 0; li < seriesList.length; li++ ) {
				legW = Math.max( legW, seriesList[ li ].title.length * 7.5 + 34 );
			}
			legW = Math.min( 220, legW );
		}

		var xAngle = Number( spec.xAxisAngle ) || 0;
		var padL = spec.hideAxes ? 12 : ( Math.max( 38, maxYLblLen * 7 + 14 ) + ( spec.yAxisTitle ? 18 : 0 ) );
		var padR = 16 + legW;
		var padT = ( spec.title || ( meta && meta.notice ) ) ? 30 : 14;
		var padB = spec.hideAxes ? 12 : ( 26 + ( Math.abs( xAngle ) > 15 ? 22 : 0 ) + ( spec.xAxisTitle ? 18 : 0 ) );
		var totalW = padL + plotW + padR;
		var totalH = Math.max( padT + plotH + padB, padT + ( showLegend ? seriesList.length * 18 + 28 : 0 ) + padB );

		var nCats = Math.max( 1, xCats.length );
		var mapX = function ( val ) {
			if ( isOrdinalX ) {
				if ( baseType === 'rect' ) {
					return padL + ( ( val + 0.5 ) / nCats ) * plotW;
				}
				return nCats <= 1 ? ( padL + plotW / 2 ) : ( padL + ( val / ( nCats - 1 ) ) * plotW );
			}
			return padL + U.applyScale( val, xMin, xMax, spec.xScaleType ) * plotW;
		};
		var mapY = function ( val ) {
			return padT + plotH - U.applyScale( val, yMin, yMax, spec.yScaleType ) * plotH;
		};

		var svg = U.createSvgEl( 'svg', {
			viewBox: '0 0 ' + totalW + ' ' + totalH,
			width: totalW,
			height: totalH,
			'class': 'graph-chart-svg',
			style: 'display:block;max-width:100%;height:auto'
		} );
		var defs = U.createSvgEl( 'defs' );
		var clipPath = U.createSvgEl( 'clipPath', { id: 'gr-async-clip' } );
		clipPath.appendChild( U.createSvgEl( 'rect', { x: padL, y: padT, width: plotW, height: plotH } ) );
		defs.appendChild( clipPath );
		svg.appendChild( defs );

		// 繪製座標軸與格線
		if ( !spec.hideAxes ) {
			for ( var yti = 0; yti < yTickObj.ticks.length; yti++ ) {
				var ytk = yTickObj.ticks[ yti ];
				if ( ytk >= yMin - 1e-9 && ytk <= yMax + 1e-9 ) {
					var py = mapY( ytk );
					if ( spec.yGrid !== false ) {
						svg.appendChild( U.createSvgEl( 'line', {
							x1: padL, y1: py.toFixed( 2 ), x2: padL + plotW, y2: py.toFixed( 2 ),
							stroke: '#e1e3e5', 'stroke-width': 1
						} ) );
					}
					svg.appendChild( U.createSvgEl( 'line', {
						x1: padL - 5, y1: py.toFixed( 2 ), x2: padL, y2: py.toFixed( 2 ),
						stroke: grey, 'stroke-width': 1
					} ) );
					svg.appendChild( U.createSvgEl( 'text', {
						x: padL - 8, y: ( py + 3.8 ).toFixed( 2 ),
						'text-anchor': 'end', 'font-size': '11px', 'font-family': 'sans-serif', fill: grey
					}, U.formatNumber( ytk, spec.yAxisFormat, spec.yType === 'integer' ) ) );
				}
			}

			if ( isOrdinalX ) {
				var stepCat = Math.max( 1, Math.ceil( nCats / Math.max( 2, Math.floor( plotW / ( Math.abs( xAngle ) > 20 ? 20 : 45 ) ) ) ) );
				for ( var ci = 0; ci < nCats; ci += stepCat ) {
					var cpx = mapX( ci );
					svg.appendChild( U.createSvgEl( 'line', {
						x1: cpx.toFixed( 2 ), y1: padT + plotH, x2: cpx.toFixed( 2 ), y2: padT + plotH + 5,
						stroke: grey, 'stroke-width': 1
					} ) );
					var txtAttrs = {
						x: cpx.toFixed( 2 ), y: padT + plotH + 15,
						'text-anchor': xAngle < 0 ? 'end' : ( xAngle > 0 ? 'start' : 'middle' ),
						'font-size': '11px', 'font-family': 'sans-serif', fill: grey
					};
					if ( xAngle !== 0 ) {
						txtAttrs.transform = 'rotate(' + xAngle + ', ' + cpx.toFixed( 2 ) + ', ' + ( padT + plotH + 12 ) + ')';
					}
					svg.appendChild( U.createSvgEl( 'text', txtAttrs, xCats[ ci ] ) );
				}
			} else {
				for ( var xti = 0; xti < xTicks.length; xti++ ) {
					var xtk = xTicks[ xti ];
					var px = mapX( xtk );
					if ( spec.xGrid ) {
						svg.appendChild( U.createSvgEl( 'line', {
							x1: px.toFixed( 2 ), y1: padT, x2: px.toFixed( 2 ), y2: padT + plotH,
							stroke: '#e1e3e5', 'stroke-width': 1
						} ) );
					}
					svg.appendChild( U.createSvgEl( 'line', {
						x1: px.toFixed( 2 ), y1: padT + plotH, x2: px.toFixed( 2 ), y2: padT + plotH + 5,
						stroke: grey, 'stroke-width': 1
					} ) );
					var xLbl = isDateX ? U.formatDateMs( xtk, spec.xAxisFormat, xMax - xMin ) : U.formatNumber( xtk, spec.xAxisFormat, spec.xType === 'integer' );
					svg.appendChild( U.createSvgEl( 'text', {
						x: px.toFixed( 2 ), y: padT + plotH + 16,
						'text-anchor': 'middle', 'font-size': '11px', 'font-family': 'sans-serif', fill: grey
					}, xLbl ) );
				}
			}
		}

		// 資料繪製層
		var plotGroup = U.createSvgEl( 'g', { 'clip-path': 'url(#gr-async-clip)' } );
		var palObj = U.buildColorAndAlphaPalette( spec.colors, baseType, nCats, seriesList.length );
		var baseY0 = mapY( Math.max( yMin, Math.min( yMax, 0 ) ) );
		var cMin = spec.domainMin !== undefined && spec.domainMin !== null ? Number( spec.domainMin ) : yMin;
		var cMax = spec.domainMax !== undefined && spec.domainMax !== null ? Number( spec.domainMax ) : yMax;
		if ( cMax <= cMin ) {
			cMax = cMin + 1;
		}

		var drawPageviewsArea = ( baseType === 'area' ) || ( spec.rawUrl && spec.rawUrl.indexOf( 'pageviews' ) !== -1 );
		var allInteractivePts = [];

		for ( var sIdx = 0; sIdx < seriesList.length; sIdx++ ) {
			var sObj = seriesList[ sIdx ];
			var c = palObj.colors[ sIdx % palObj.colors.length ];
			var alpha = palObj.alphas[ sIdx % palObj.alphas.length ] || 1;
			var sCmap = ( spec.seriesColorMaps && spec.seriesColorMaps[ String( sIdx + 1 ) ] ) || spec.colorMap;
			var hasCmap = sCmap && U.isColormapName( sCmap );
			var baseCol = ( sCmap && !hasCmap && U.isValidColorToken( sCmap ) ) ? sCmap : c;
			var lw = spec.linewidth !== undefined && spec.linewidth !== null ? Number( spec.linewidth ) : ( baseType === 'area' ? 1.8 : 2.5 );

			if ( baseType === 'rect' ) {
				var padRatio = spec.barPadding !== undefined && spec.barPadding !== null ? Number( spec.barPadding ) : 0.18;
				var bandW = ( plotW / Math.max( 1, isOrdinalX ? nCats : sObj.pts.length ) ) * ( 1 - padRatio );
				var gap = ( !isStacked && seriesList.length > 1 ) ? 1.5 : 0;
				var subBarW = isStacked ? bandW : Math.max( 1.5, ( bandW - ( seriesList.length - 1 ) * gap ) / seriesList.length );

				for ( var bi = 0; bi < sObj.pts.length; bi++ ) {
					var bPt = sObj.pts[ bi ];
					var centerPx = mapX( bPt.x );
					var barX = isStacked ? ( centerPx - bandW / 2 ) : ( centerPx - bandW / 2 + sIdx * ( subBarW + gap ) );
					var py1 = mapY( bPt.y1 );
					var py0 = isStacked ? mapY( bPt.y0 ) : baseY0;
					var topY = Math.min( py1, py0 );
					var rectH = Math.max( 1, Math.abs( py0 - py1 ) );
					var barCol = hasCmap ? U.sampleColormap( ( bPt.y - cMin ) / ( cMax - cMin ), sCmap ) : baseCol;

					plotGroup.appendChild( U.createSvgEl( 'rect', {
						x: barX.toFixed( 2 ), y: topY.toFixed( 2 ),
						width: subBarW.toFixed( 2 ), height: rectH.toFixed( 2 ),
						fill: barCol, 'fill-opacity': alpha < 1 ? alpha.toFixed( 2 ) : '0.9'
					} ) );

					bPt.sx = barX + subBarW / 2;
					bPt.sy = topY;
					bPt.color = barCol;
					bPt.seriesTitle = sObj.title;
					allInteractivePts.push( bPt );
				}
			} else {
				var pathCol = baseCol;
				if ( hasCmap ) {
					var gradId = 'gr-async-cmap-' + sIdx + '-' + Math.floor( Math.random() * 10000 );
					defs.appendChild( U.buildColormapGradientEl( gradId, sCmap, 0, mapY( cMin ), 0, mapY( cMax ), true ) );
					pathCol = 'url(#' + gradId + ')';
				}

				var topCoords = [];
				var botCoords = [];
				for ( var pi2 = 0; pi2 < sObj.pts.length; pi2++ ) {
					var pt2 = sObj.pts[ pi2 ];
					var sx = mapX( pt2.x );
					var sy1 = mapY( pt2.y1 );
					var sy0 = mapY( pt2.y0 );
					var ptCol = hasCmap ? U.sampleColormap( ( pt2.y - cMin ) / ( cMax - cMin ), sCmap ) : baseCol;
					pt2.sx = sx;
					pt2.sy = sy1;
					pt2.color = ptCol;
					pt2.seriesTitle = sObj.title;
					topCoords.push( [ sx, sy1 ] );
					botCoords.push( [ sx, sy0 ] );
					allInteractivePts.push( pt2 );

					if ( spec.showSymbols || lw === 0 || pt2.size !== null ) {
						var symR = 3.8;
						if ( pt2.size !== null && !isNaN( pt2.size ) ) {
							symR = sizeMax > sizeMin ? ( 4 + Math.sqrt( ( pt2.size - sizeMin ) / ( sizeMax - sizeMin ) ) * 12 ) : 6;
						}
						plotGroup.appendChild( U.createSymbolEl(
							spec.symbolsShape || 'circle',
							sx, sy1, symR,
							spec.symbolsNoFill ? 'none' : ptCol,
							ptCol,
							spec.symbolsStroke || ( spec.symbolsNoFill ? 1.8 : 0 ),
							pt2.size !== null ? 0.78 : alpha
						) );
					}
				}

				if ( topCoords.length >= 2 ) {
					var topD = U.buildPathD( topCoords, spec.interpolate || 'monotone', false );
					if ( drawPageviewsArea ) {
						var areaD;
						if ( isStacked ) {
							areaD = topD + ' ' + U.buildPathD( botCoords, spec.interpolate || 'monotone', true ) + ' Z';
						} else {
							var firstX = topCoords[ 0 ][ 0 ].toFixed( 2 );
							var lastX = topCoords[ topCoords.length - 1 ][ 0 ].toFixed( 2 );
							areaD = topD + ' L ' + lastX + ',' + baseY0.toFixed( 2 ) + ' L ' + firstX + ',' + baseY0.toFixed( 2 ) + ' Z';
						}
						plotGroup.appendChild( U.createSvgEl( 'path', {
							d: areaD, fill: pathCol, 'fill-opacity': isStacked ? '0.8' : '0.30', stroke: 'none'
						} ) );
					}
					if ( lw > 0 ) {
						plotGroup.appendChild( U.createSvgEl( 'path', {
							d: topD, fill: 'none', stroke: pathCol, 'stroke-width': lw,
							'stroke-linecap': 'round', 'stroke-linejoin': 'round'
						} ) );
					}
				}
			}
		}

		svg.appendChild( plotGroup );

		if ( !spec.hideAxes ) {
			svg.appendChild( U.createSvgEl( 'polyline', {
				points: padL + ',' + padT + ' ' + padL + ',' + ( padT + plotH ) + ' ' + ( padL + plotW ) + ',' + ( padT + plotH ),
				fill: 'none', stroke: grey, 'stroke-width': 1.8
			} ) );
		}

		var titleText = spec.title || ( meta && meta.notice ) || '';
		if ( titleText ) {
			svg.appendChild( U.createSvgEl( 'text', {
				x: ( padL + plotW / 2 ).toFixed( 1 ), y: 20,
				'text-anchor': 'middle', 'font-size': '13px', 'font-weight': 'bold', 'font-family': 'sans-serif', fill: grey
			}, titleText ) );
		}
		if ( spec.xAxisTitle && !spec.hideAxes ) {
			svg.appendChild( U.createSvgEl( 'text', {
				x: ( padL + plotW / 2 ).toFixed( 1 ), y: totalH - 6,
				'text-anchor': 'middle', 'font-size': '12px', 'font-weight': 'bold', 'font-family': 'sans-serif', fill: grey
			}, spec.xAxisTitle ) );
		}
		if ( spec.yAxisTitle && !spec.hideAxes ) {
			var yc = padT + plotH / 2;
			svg.appendChild( U.createSvgEl( 'text', {
				x: 14, y: yc.toFixed( 1 ),
				'text-anchor': 'middle', 'font-size': '12px', 'font-weight': 'bold', 'font-family': 'sans-serif',
				fill: grey, transform: 'rotate(-90, 14, ' + yc.toFixed( 1 ) + ')'
			}, spec.yAxisTitle ) );
		}

		if ( showLegend && seriesList.length > 0 ) {
			var legX = padL + plotW + 14;
			var legY = padT + 4;
			var legTitle = String( spec.legend ).trim();
			if ( legTitle !== '' && legTitle.toLowerCase() !== 'true' && legTitle.toLowerCase() !== 'yes' && legTitle !== '1' ) {
				svg.appendChild( U.createSvgEl( 'text', {
					x: legX, y: legY + 10, 'font-size': '12px', 'font-weight': 'bold', 'font-family': 'sans-serif', fill: grey
				}, legTitle ) );
				legY += 20;
			}
			for ( var lIdx = 0; lIdx < seriesList.length; lIdx++ ) {
				var iy = legY + lIdx * 18;
				var lCol = palObj.colors[ lIdx % palObj.colors.length ];
				if ( baseType === 'line' ) {
					svg.appendChild( U.createSvgEl( 'line', {
						x1: legX, y1: iy + 5.5, x2: legX + 14, y2: iy + 5.5, stroke: lCol, 'stroke-width': 2.5
					} ) );
				} else {
					svg.appendChild( U.createSvgEl( 'rect', {
						x: legX, y: iy, width: 12, height: 11, rx: 2, fill: lCol
					} ) );
				}
				svg.appendChild( U.createSvgEl( 'text', {
					x: legX + 18, y: iy + 9.5, 'font-size': '11px', 'font-family': 'sans-serif', fill: grey
				}, seriesList[ lIdx ].title ) );
			}
		}

		// 互動十字準線與 Tooltip
		var guideLine = U.createSvgEl( 'line', {
			x1: padL, y1: padT, x2: padL, y2: padT + plotH,
			stroke: '#888888', 'stroke-width': 1, 'stroke-dasharray': '3,3',
			style: 'display:none;pointer-events:none'
		} );
		var focusDot = U.createSvgEl( 'circle', {
			cx: padL, cy: padT + plotH, r: 4.5,
			fill: palObj.colors[ 0 ], stroke: '#ffffff', 'stroke-width': 1.8,
			style: 'display:none;pointer-events:none'
		} );
		svg.appendChild( guideLine );
		svg.appendChild( focusDot );

		var stageWrap = U.createHtmlEl(
			'div',
			'svg-graph gr-async-stage',
			'position:relative;display:block;clear:both;width:' + totalW + 'px;max-width:100%;margin:0.5em 0'
		);
		var tipUI = U.createTooltipUI();

		svg.addEventListener( 'mousemove', function ( ev ) {
			var rect = svg.getBoundingClientRect();
			var scaleX = totalW / Math.max( 1, rect.width );
			var scaleY = totalH / Math.max( 1, rect.height );
			var mx = ( ev.clientX - rect.left ) * scaleX;
			var my = ( ev.clientY - rect.top ) * scaleY;
			if ( mx < padL - 4 || mx > padL + plotW + 4 ) {
				guideLine.style.display = 'none';
				focusDot.style.display = 'none';
				tipUI.hide();
				return;
			}
			var bestPt = null;
			var bestScore = Infinity;
			for ( var i = 0; i < allInteractivePts.length; i++ ) {
				var pt = allInteractivePts[ i ];
				var dx = Math.abs( pt.sx - mx );
				var dy = Math.abs( pt.sy - my );
				var score = seriesList.length > 1 ? ( dx * 2 + dy * 0.5 ) : dx;
				if ( score < bestScore ) {
					bestScore = score;
					bestPt = pt;
				}
			}
			if ( bestPt ) {
				guideLine.setAttribute( 'x1', bestPt.sx.toFixed( 2 ) );
				guideLine.setAttribute( 'x2', bestPt.sx.toFixed( 2 ) );
				guideLine.style.display = '';
				focusDot.setAttribute( 'cx', bestPt.sx.toFixed( 2 ) );
				focusDot.setAttribute( 'cy', bestPt.sy.toFixed( 2 ) );
				focusDot.setAttribute( 'fill', bestPt.color );
				focusDot.style.display = '';

				var xStr = isOrdinalX ? ( xCats[ bestPt.x ] || String( bestPt.rawX ) ) :
					( isDateX ? U.formatDateMs( bestPt.x, spec.xAxisFormat, 0 ) : U.formatNumber( bestPt.x, spec.xAxisFormat, spec.xType === 'integer' ) );
				var yStr = U.formatNumber( bestPt.y, spec.yAxisFormat, spec.yType === 'integer' );
				var lbl = seriesList.length > 1 ? ( bestPt.seriesTitle + ' (' + xStr + ')' ) : xStr;
				tipUI.show( bestPt.sx, bestPt.sy, totalW, totalH, bestPt.color, lbl, yStr );
			}
		} );

		svg.addEventListener( 'mouseleave', function () {
			guideLine.style.display = 'none';
			focusDot.style.display = 'none';
			tipUI.hide();
		} );

		stageWrap.appendChild( svg );
		stageWrap.appendChild( tipUI.el );
		U.mountStage( container, stageWrap );
		return true;
	}

	var types2D = [
		'line', 'area', 'stackedline', 'stackedarea',
		'rect', 'bar', 'groupedbar', 'groupedrect', 'stackedrect', 'stackedbar',
		'hist', 'histogram', 'groupedhist', 'scatter', 'bubble'
	];
	for ( var i = 0; i < types2D.length; i++ ) {
		GW.Renderers[ types2D[ i ] ] = renderCartesian2D;
	}
	GW.Chart2D = renderCartesian2D;
}() );