User:A2569875/Module:Graph/RenderUtils.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Module:Graph/RenderUtils.js
( function () {
'use strict';
window.mw = window.mw || {};
mw.libs = mw.libs || {};
var GW = mw.libs.GraphWidget = mw.libs.GraphWidget || {};
GW.Renderers = GW.Renderers || {};
var SVG_NS = 'http://www.w3.org/2000/svg';
var Utils = {};
Utils.SVG_NS = SVG_NS;
Utils.PALETTE_CAT10 = [
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'
];
Utils.PALETTE_CAT20 = [
'#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c',
'#98df8a', '#d62728', '#ff9896', '#9467bd', '#c5b0d5',
'#8c564b', '#c49c94', '#e377c2', '#f7b6d2', '#7f7f7f',
'#c7c7c7', '#bcbd22', '#dbdb8d', '#17becf', '#9edae5'
];
var CSS_COLORS = {
white: [ 255, 255, 255 ], black: [ 0, 0, 0 ], silver: [ 192, 192, 192 ],
gray: [ 128, 128, 128 ], grey: [ 128, 128, 128 ], red: [ 255, 0, 0 ],
darkred: [ 139, 0, 0 ], green: [ 0, 128, 0 ], blue: [ 0, 0, 255 ],
darkblue: [ 0, 0, 139 ], lightblue: [ 173, 216, 230 ], steelblue: [ 70, 130, 180 ],
yellow: [ 255, 255, 0 ], gold: [ 255, 215, 0 ], orange: [ 255, 165, 0 ],
purple: [ 128, 0, 128 ], pink: [ 255, 192, 203 ], brown: [ 165, 42, 42 ],
cyan: [ 0, 255, 255 ], teal: [ 0, 128, 128 ], navy: [ 0, 0, 128 ]
};
Utils.SHAPES = {
circle: 'circle',
x: 'M-.5,-.5L.5,.5M.5,-.5L-.5,.5',
square: 'square',
cross: 'cross',
diamond: 'diamond',
triangle: 'triangle-up',
triangle_up: 'triangle-up',
triangle_down: 'triangle-down',
triangle_right: 'triangle-right',
triangle_left: 'triangle-left',
'triangle-up': 'triangle-up',
'triangle-down': 'triangle-down',
'triangle-right': 'triangle-right',
'triangle-left': 'triangle-left',
star: 'M 0,-0.5 L 0.112,-0.154 L 0.475,-0.154 L 0.181,0.059 L 0.293,0.404 L 0,0.191 L -0.293,0.404 L -0.181,0.059 L -0.475,-0.154 L -0.112,-0.154 Z',
wye: 'M 0,-0.5 L 0.12,-0.5 L 0.12,-0.08 L 0.48,0.28 L 0.36,0.40 L 0,0.06 L -0.36,0.40 L -0.48,0.28 L -0.12,-0.08 L -0.12,-0.5 Z',
ring: 'M 0,-0.45 A 0.45,0.45 0 1,0 0,0.45 A 0.45,0.45 0 1,0 0,-0.45 M 0,-0.25 A 0.25,0.25 0 1,1 0,0.25 A 0.25,0.25 0 1,1 0,-0.25 Z',
plus: 'M -0.12,-0.45 H 0.12 V -0.12 H 0.45 V 0.12 H 0.12 V 0.45 H -0.12 V 0.12 H -0.45 V -0.12 H -0.12 Z',
banana: 'm -0.5281,0.2880 0.0020,0.0192 m 0,0 c 0.1253,0.0543 0.2118,0.0679 0.3268,0.0252 0.1569,-0.0582 0.3663,-0.1636 0.4607,-0.3407 0.0824,-0.1547 0.1202,-0.2850 0.0838,-0.4794 l 0.0111,-0.1498 -0.0457,-0.0015 c -0.0024,0.3045 -0.1205,0.5674 -0.3357,0.7414 -0.1409,0.1139 -0.3227,0.1693 -0.5031,0.1856 m 0,0 c 0.1804,-0.0163 0.3622,-0.0717 0.5031,-0.1856 0.2152,-0.1739 0.3329,-0.4291 0.3357,-0.7414 l -0.0422,0.0079 c 0,0 -0.0099,0.1111 -0.0227,0.1644 -0.0537,0.1937 -0.1918,0.3355 -0.3349,0.4481 -0.1393,0.1089 -0.2717,0.2072 -0.4326,0.2806 l -0.0062,0.0260'
};
var MONTH_ABBR = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
var MONTH_FULL = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
// =========================================================================
// 1. 安全 DOM 與 SVG 建構器 (強制使用 textContent,禁止 innerHTML)
// =========================================================================
Utils.createSvgEl = function ( tag, attrs, text ) {
var el = document.createElementNS( SVG_NS, tag );
if ( attrs ) {
for ( var k in attrs ) {
if ( Object.prototype.hasOwnProperty.call( attrs, k ) && attrs[ k ] !== undefined && attrs[ k ] !== null ) {
el.setAttribute( k, String( attrs[ k ] ) );
}
}
}
if ( text !== undefined && text !== null ) {
el.textContent = String( text );
}
return el;
};
Utils.createHtmlEl = function ( tag, className, cssText, text ) {
var el = document.createElement( tag );
if ( className ) {
el.className = className;
}
if ( cssText ) {
el.style.cssText = cssText;
}
if ( text !== undefined && text !== null ) {
el.textContent = String( text );
}
return el;
};
// 清除容器內預設的 ombox 警告框,但嚴格保留 MediaWiki 的 <style data-mw-deduplicate>、<link> 與 .gr-async-i18n 繁簡轉換字典
Utils.mountStage = function ( container, stageWrap ) {
var child = container.firstChild;
while ( child ) {
var next = child.nextSibling;
var tag = child.nodeName ? child.nodeName.toLowerCase() : '';
var isI18n = child.nodeType === 1 && child.classList && child.classList.contains( 'gr-async-i18n' );
if ( tag !== 'style' && tag !== 'link' && !isI18n ) {
container.removeChild( child );
}
child = next;
}
container.style.display = 'block';
container.style.clear = 'both';
container.appendChild( stageWrap );
};
// =========================================================================
// 2. 數值與日期格式化 (對應 RenderUtils.formatNumber / formatDateValue)
// =========================================================================
function addCommas( numStr ) {
var m = String( numStr ).match( /^([+-]?)(\d+)(\.?\d*)$/ );
if ( !m ) {
return String( numStr );
}
var intPart = m[ 2 ].replace( /\B(?=(\d{3})+(?!\d))/g, ',' );
return m[ 1 ] + intPart + m[ 3 ];
}
Utils.formatNumber = function ( val, fmt, isInt ) {
var n = Number( val );
if ( isNaN( n ) ) {
return String( val === undefined || val === null ? '' : val );
}
if ( Math.abs( n ) < 1e-12 ) {
n = 0;
}
if ( !fmt || fmt === '' ) {
if ( isInt || Math.abs( n - Math.round( n ) ) < 1e-7 ) {
return addCommas( String( Math.round( n ) ) );
}
return addCommas( n.toFixed( 2 ).replace( /\.?0+$/, '' ) );
}
var sFmt = String( fmt );
var useComma = sFmt.indexOf( ',' ) !== -1;
var isPct = sFmt.indexOf( '%' ) !== -1;
var isPermille = sFmt.indexOf( '‰' ) !== -1;
var isSI = sFmt.indexOf( 's' ) !== -1;
var precMatch = sFmt.match( /\.(\d+)/ );
var prec = precMatch ? parseInt( precMatch[ 1 ], 10 ) : null;
if ( isPct ) {
n *= 100;
} else if ( isPermille ) {
n *= 1000;
}
if ( Math.abs( n ) < 1e-12 ) {
n = 0;
}
var suffix = '';
if ( isSI && n !== 0 ) {
var absN = Math.abs( n );
if ( absN >= 1e9 ) {
n /= 1e9;
suffix = 'G';
} else if ( absN >= 1e6 ) {
n /= 1e6;
suffix = 'M';
} else if ( absN >= 1e3 ) {
n /= 1e3;
suffix = 'k';
}
}
var out;
if ( prec !== null ) {
out = n.toFixed( prec );
} else if ( ( isInt && !isPct && !isPermille ) || Math.abs( n - Math.round( n ) ) < 1e-7 ) {
out = String( Math.round( n ) );
} else {
out = n.toFixed( 2 ).replace( /\.?0+$/, '' );
}
if ( useComma ) {
out = addCommas( out );
}
if ( isPct ) {
out += '%';
} else if ( isPermille ) {
out += '‰';
}
return out + suffix;
};
Utils.parseDateMs = function ( v ) {
if ( v === null || v === undefined || v === false ) {
return NaN;
}
if ( typeof v === 'number' ) {
if ( v > 1e8 ) {
return v;
}
var yr = Math.floor( v );
var rem = v - yr;
return Date.UTC( yr, 0, 1 ) + rem * 365.25 * 86400000;
}
var s = String( v ).trim();
var mTs = s.match( /^(\d{4})(\d{2})(\d{2})(?:\d{2})?$/ );
if ( mTs ) {
return Date.UTC( Number( mTs[ 1 ] ), Number( mTs[ 2 ] ) - 1, Number( mTs[ 3 ] ) );
}
var mIso = s.match( /^(\d{4})[-/](\d{1,2})(?:[-/](\d{1,2}))?/ );
if ( mIso ) {
return Date.UTC( Number( mIso[ 1 ] ), Number( mIso[ 2 ] ) - 1, Number( mIso[ 3 ] || 1 ) );
}
var parsed = Date.parse( s );
return isNaN( parsed ) ? Number( s ) : parsed;
};
Utils.formatDateMs = function ( ms, fmt, spanMs ) {
var d = new Date( ms );
if ( isNaN( d.getTime() ) ) {
return String( ms );
}
var y = d.getUTCFullYear();
var m = d.getUTCMonth() + 1;
var day = d.getUTCDate();
var mm = ( m < 10 ? '0' : '' ) + m;
var dd = ( day < 10 ? '0' : '' ) + day;
if ( !fmt || fmt === '' ) {
if ( spanMs !== undefined && spanMs > 86400000 * 365 * 4 ) {
return String( y );
}
if ( spanMs !== undefined && spanMs > 86400000 * 365 * 1.5 ) {
return y + '-' + mm;
}
return y + '-' + mm + '-' + dd;
}
return String( fmt )
.replace( /%Y/g, String( y ) )
.replace( /%y/g, String( y % 100 ).replace( /^(\d)$/, '0$1' ) )
.replace( /%B/g, MONTH_FULL[ m - 1 ] || '' )
.replace( /%b/g, MONTH_ABBR[ m - 1 ] || '' )
.replace( /%m/g, mm )
.replace( /%d/g, dd );
};
// =========================================================================
// 3. 比例尺與座標軸刻度計算 (applyScale / computeNiceTicks)
// =========================================================================
Utils.applyScale = function ( val, minV, maxV, scaleType ) {
if ( maxV <= minV ) {
return 0.5;
}
var sType = String( scaleType || 'linear' ).toLowerCase();
if ( sType === 'log' ) {
var lMin = Math.log( Math.max( 1e-9, minV ) ) / Math.LN10;
var lMax = Math.log( Math.max( 1e-8, maxV ) ) / Math.LN10;
if ( lMax <= lMin ) {
return 0.5;
}
var lVal = Math.log( Math.max( 1e-9, val ) ) / Math.LN10;
return ( lVal - lMin ) / ( lMax - lMin );
} else if ( sType === 'sqrt' ) {
var sMin = Math.sqrt( Math.max( 0, minV ) );
var sMax = Math.sqrt( Math.max( 1e-9, maxV ) );
return ( Math.sqrt( Math.max( 0, val ) ) - sMin ) / ( sMax - sMin );
} else if ( sType.indexOf( 'pow' ) === 0 ) {
var mExp = sType.match( /([\d.]+)/ );
var expN = mExp ? Number( mExp[ 1 ] ) : 2;
var pMin = ( minV >= 0 ? 1 : -1 ) * Math.pow( Math.abs( minV ), expN );
var pMax = ( maxV >= 0 ? 1 : -1 ) * Math.pow( Math.abs( maxV ), expN );
var pVal = ( val >= 0 ? 1 : -1 ) * Math.pow( Math.abs( val ), expN );
return ( pVal - pMin ) / ( pMax - pMin );
}
return ( val - minV ) / ( maxV - minV );
};
Utils.computeNiceTicks = function ( minV, maxV, targetCount, scaleType, isInteger ) {
targetCount = Math.max( 2, Number( targetCount ) || 5 );
if ( minV === maxV ) {
if ( minV === 0 ) {
maxV = 10;
} else {
minV -= 1;
maxV += 1;
}
}
var sType = String( scaleType || 'linear' ).toLowerCase();
if ( sType === 'log' && minV > 0 && maxV > minV ) {
var pMin = Math.floor( Math.log( minV ) / Math.LN10 + 1e-9 );
var pMax = Math.ceil( Math.log( maxV ) / Math.LN10 - 1e-9 );
var logTicks = [];
for ( var e = pMin; e <= pMax; e++ ) {
logTicks.push( Math.pow( 10, e ) );
}
if ( logTicks.length >= 2 ) {
return {
ticks: logTicks,
min: Math.min( minV, logTicks[ 0 ] ),
max: Math.max( maxV, logTicks[ logTicks.length - 1 ] )
};
}
}
var span = maxV - minV;
var rawStep = span / targetCount;
var mag = Math.pow( 10, Math.floor( Math.log( Math.max( 1e-12, rawStep ) ) / Math.LN10 ) );
var norm = rawStep / mag;
var step;
if ( norm <= 1.5 ) {
step = 1 * mag;
} else if ( norm <= 3.0 ) {
step = 2 * mag;
} else if ( norm <= 7.0 ) {
step = 5 * mag;
} else {
step = 10 * mag;
}
if ( isInteger && step < 1 ) {
step = 1;
}
var niceMin = Math.floor( minV / step + 1e-9 ) * step;
var niceMax = Math.ceil( maxV / step - 1e-9 ) * step;
var ticks = [];
var guard = 0;
for ( var cur = niceMin; cur <= niceMax + step * 0.001 && guard < 100; cur += step ) {
ticks.push( Math.abs( cur ) < 1e-11 ? 0 : cur );
guard++;
}
return { ticks: ticks, min: niceMin, max: niceMax };
};
// =========================================================================
// 4. 顏色與科學色階系統 (支援內建、方案 A 與方案 B 之漸層式/離散式色階)
// =========================================================================
Utils.isValidColorToken = function ( cStr ) {
if ( typeof cStr !== 'string' ) {
return false;
}
var s = cStr.trim().toLowerCase();
if ( !s || s === 'category10' || s === 'category20' ) {
return false;
}
if ( CSS_COLORS[ s ] ) {
return true;
}
if ( /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test( s ) ) {
return true;
}
if ( /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)$/i.test( s ) ) {
return true;
}
return false;
};
Utils.parseRGB = function ( cStr ) {
if ( !cStr ) {
return [ 128, 128, 128 ];
}
var s = String( cStr ).trim().toLowerCase();
if ( CSS_COLORS[ s ] ) {
return CSS_COLORS[ s ].slice();
}
var m6 = s.match( /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i );
if ( m6 ) {
return [ parseInt( m6[ 1 ], 16 ), parseInt( m6[ 2 ], 16 ), parseInt( m6[ 3 ], 16 ) ];
}
var m8 = s.match( /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i );
if ( m8 ) {
return [ parseInt( m8[ 2 ], 16 ), parseInt( m8[ 3 ], 16 ), parseInt( m8[ 4 ], 16 ) ];
}
var m3 = s.match( /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i );
if ( m3 ) {
return [
parseInt( m3[ 1 ] + m3[ 1 ], 16 ),
parseInt( m3[ 2 ] + m3[ 2 ], 16 ),
parseInt( m3[ 3 ] + m3[ 3 ], 16 )
];
}
var mRgb = s.match( /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i );
if ( mRgb ) {
return [ Number( mRgb[ 1 ] ), Number( mRgb[ 2 ] ), Number( mRgb[ 3 ] ) ];
}
return [ 128, 128, 128 ];
};
Utils.rgbToHex = function ( r, g, b ) {
var toHex = function ( n ) {
var v = Math.max( 0, Math.min( 255, Math.round( n ) ) );
return ( v < 16 ? '0' : '' ) + v.toString( 16 );
};
return '#' + toHex( r ) + toHex( g ) + toHex( b );
};
Utils.buildColorAndAlphaPalette = function ( colorsInput, chartType, xCount, yCount ) {
var base = null;
if ( Array.isArray( colorsInput ) && colorsInput.length > 0 ) {
base = colorsInput;
} else if ( typeof colorsInput === 'string' && colorsInput.trim() !== '' ) {
var splitCols = colorsInput.split( ',' );
var validCols = [];
for ( var i = 0; i < splitCols.length; i++ ) {
var tk = splitCols[ i ].trim();
if ( Utils.isValidColorToken( tk ) ) {
validCols.push( tk );
}
}
if ( validCols.length > 0 ) {
base = validCols;
}
}
if ( !base ) {
base = ( ( chartType === 'pie' && xCount > 10 ) || yCount > 10 ) ? Utils.PALETTE_CAT20 : Utils.PALETTE_CAT10;
}
var colors = [];
var alphas = [];
for ( var j = 0; j < base.length; j++ ) {
var c = String( base[ j ] || '' ).trim();
var m8 = c.match( /^#([0-9a-fA-F]{2})([0-9a-fA-F]{6})$/ );
if ( m8 ) {
colors.push( '#' + m8[ 2 ] );
alphas.push( parseInt( m8[ 1 ], 16 ) / 255 );
} else {
colors.push( c );
alphas.push( 1.0 );
}
}
return { colors: colors, alphas: alphas };
};
Utils.COLORMAPS = {
viridis: [ [ 68, 1, 84 ], [ 59, 82, 139 ], [ 33, 145, 140 ], [ 94, 201, 98 ], [ 253, 231, 37 ] ],
coolwarm: [ [ 59, 76, 192 ], [ 141, 176, 254 ], [ 221, 221, 221 ], [ 244, 154, 123 ], [ 180, 4, 38 ] ],
plasma: [ [ 13, 8, 135 ], [ 126, 3, 168 ], [ 204, 71, 120 ], [ 248, 149, 64 ], [ 240, 249, 33 ] ],
inferno: [ [ 0, 0, 4 ], [ 87, 16, 110 ], [ 188, 55, 84 ], [ 249, 142, 9 ], [ 252, 255, 164 ] ],
magma: [ [ 0, 0, 4 ], [ 81, 18, 124 ], [ 183, 55, 121 ], [ 251, 136, 97 ], [ 252, 253, 191 ] ],
rainbow: [ [ 48, 18, 59 ], [ 40, 188, 235 ], [ 164, 252, 60 ], [ 251, 126, 33 ], [ 122, 4, 3 ] ],
gray: [ [ 0, 0, 0 ], [ 255, 255, 255 ] ],
greys: [ [ 255, 255, 255 ], [ 0, 0, 0 ] ]
};
function evalJetRGB( t ) {
var rf, gf, bf;
if ( t <= 0.35 ) {
rf = 0;
} else if ( t <= 0.66 ) {
rf = ( t - 0.35 ) / 0.31;
} else if ( t <= 0.89 ) {
rf = 1;
} else {
rf = 1 - 0.5 * ( t - 0.89 ) / 0.11;
}
if ( t <= 0.125 ) {
gf = 0;
} else if ( t <= 0.375 ) {
gf = ( t - 0.125 ) / 0.25;
} else if ( t <= 0.64 ) {
gf = 1;
} else if ( t <= 0.91 ) {
gf = 1 - ( t - 0.64 ) / 0.27;
} else {
gf = 0;
}
if ( t <= 0.11 ) {
bf = 0.5 + 0.5 * ( t / 0.11 );
} else if ( t <= 0.34 ) {
bf = 1;
} else if ( t <= 0.65 ) {
bf = 1 - ( t - 0.34 ) / 0.31;
} else {
bf = 0;
}
return [ rf * 255, gf * 255, bf * 255 ];
}
var cmapCache = {};
Utils.parseColormapSpec = function ( rawName ) {
if ( typeof rawName !== 'string' || !rawName.trim() ) {
return null;
}
var s = rawName.trim();
if ( cmapCache[ s ] !== undefined ) {
return cmapCache[ s ];
}
var body = s;
var isDiscrete = false;
var mPre = body.match( /^([a-zA-Z]+)\s*:\s*(.+)$/ );
if ( mPre ) {
var lp = mPre[ 1 ].toLowerCase();
if ( lp === 'discrete' || lp === 'step' || lp === 'steps' ) {
isDiscrete = true;
body = mPre[ 2 ].trim();
} else if ( lp === 'gradient' || lp === 'continuous' || lp === 'linear' ) {
body = mPre[ 2 ].trim();
}
}
var mBr = body.match( /^\[\s*(.*?)\s*\]$/ );
if ( mBr ) {
body = mBr[ 1 ];
}
var cname = body.toLowerCase();
if ( cname === 'jet' ) {
cmapCache[ s ] = { kind: 'jet', isDiscrete: isDiscrete, nSteps: 7 };
return cmapCache[ s ];
}
if ( Utils.COLORMAPS[ cname ] ) {
var builtIn = Utils.COLORMAPS[ cname ];
cmapCache[ s ] = { kind: 'stops', stops: builtIn, isDiscrete: isDiscrete, nSteps: builtIn.length };
return cmapCache[ s ];
}
var sep = null;
if ( body.indexOf( ';' ) !== -1 ) {
sep = ';';
isDiscrete = true;
} else if ( body.indexOf( ':' ) !== -1 ) {
sep = ':';
} else if ( body.indexOf( ',' ) !== -1 ) {
sep = ',';
}
if ( sep ) {
var parts = body.split( sep );
if ( parts.length >= 2 ) {
var stops = [];
var ok = true;
for ( var i = 0; i < parts.length; i++ ) {
var tk = parts[ i ].trim();
if ( Utils.isValidColorToken( tk ) ) {
stops.push( Utils.parseRGB( tk ) );
} else {
ok = false;
break;
}
}
if ( ok && stops.length >= 2 ) {
cmapCache[ s ] = { kind: 'stops', stops: stops, isDiscrete: isDiscrete, nSteps: stops.length };
return cmapCache[ s ];
}
}
}
cmapCache[ s ] = null;
return null;
};
Utils.isColormapName = function ( name ) {
return Utils.parseColormapSpec( name ) !== null;
};
Utils.sampleColormap = function ( t, mapName ) {
t = Math.max( 0, Math.min( 1, Number( t ) || 0 ) );
var spec = Utils.parseColormapSpec( mapName ) || Utils.parseColormapSpec( 'viridis' );
if ( spec.isDiscrete ) {
var nSteps = Math.max( 2, spec.nSteps || ( spec.stops && spec.stops.length ) || 5 );
var bucket = Math.min( nSteps, Math.max( 1, Math.floor( t * nSteps ) + 1 ) );
if ( spec.kind === 'jet' ) {
var bt = ( bucket - 1 ) / Math.max( 1, nSteps - 1 );
var jRgb = evalJetRGB( bt );
return Utils.rgbToHex( jRgb[ 0 ], jRgb[ 1 ], jRgb[ 2 ] );
}
var dCol = spec.stops[ Math.min( spec.stops.length - 1, bucket - 1 ) ];
return Utils.rgbToHex( dCol[ 0 ], dCol[ 1 ], dCol[ 2 ] );
}
if ( spec.kind === 'jet' ) {
var rgb = evalJetRGB( t );
return Utils.rgbToHex( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ] );
}
var stops = spec.stops || Utils.COLORMAPS.viridis;
var n = stops.length;
var scaled = t * ( n - 1 );
var idx = Math.min( n - 2, Math.floor( scaled ) );
var frac = scaled - idx;
var c0 = stops[ idx ], c1 = stops[ idx + 1 ];
return Utils.rgbToHex(
c0[ 0 ] + ( c1[ 0 ] - c0[ 0 ] ) * frac,
c0[ 1 ] + ( c1[ 1 ] - c0[ 1 ] ) * frac,
c0[ 2 ] + ( c1[ 2 ] - c0[ 2 ] ) * frac
);
};
Utils.interpolateColorRamp = function ( val, domain, range ) {
if ( !Array.isArray( domain ) || !Array.isArray( range ) || domain.length === 0 || range.length === 0 ) {
return '#3182bd';
}
var v = Number( val ) || 0;
if ( v <= Number( domain[ 0 ] ) ) {
return range[ 0 ];
}
var n = Math.min( domain.length, range.length );
if ( v >= Number( domain[ n - 1 ] ) ) {
return range[ n - 1 ];
}
for ( var i = 0; i < n - 1; i++ ) {
var d0 = Number( domain[ i ] ), d1 = Number( domain[ i + 1 ] );
if ( v >= d0 && v <= d1 ) {
var t = d1 > d0 ? ( v - d0 ) / ( d1 - d0 ) : 0;
var c0 = Utils.parseRGB( range[ i ] );
var c1 = Utils.parseRGB( range[ i + 1 ] );
return Utils.rgbToHex(
c0[ 0 ] + ( c1[ 0 ] - c0[ 0 ] ) * t,
c0[ 1 ] + ( c1[ 1 ] - c0[ 1 ] ) * t,
c0[ 2 ] + ( c1[ 2 ] - c0[ 2 ] ) * t
);
}
}
return range[ n - 1 ];
};
function appendGradientStops( gradEl, mapName ) {
var spec = Utils.parseColormapSpec( mapName );
if ( spec && spec.isDiscrete ) {
var n = Math.max( 2, spec.nSteps || ( spec.stops && spec.stops.length ) || 5 );
for ( var k = 1; k <= n; k++ ) {
var p0 = Math.round( ( ( k - 1 ) / n ) * 100 );
var p1 = Math.round( ( k / n ) * 100 );
var col = Utils.sampleColormap( ( k - 0.5 ) / n, mapName );
gradEl.appendChild( Utils.createSvgEl( 'stop', { offset: p0 + '%', 'stop-color': col } ) );
gradEl.appendChild( Utils.createSvgEl( 'stop', { offset: p1 + '%', 'stop-color': col } ) );
}
} else {
var nStops = ( spec && spec.stops && spec.stops.length > 10 ) ? spec.stops.length : 10;
for ( var s = 0; s <= nStops; s++ ) {
var t = s / nStops;
gradEl.appendChild( Utils.createSvgEl( 'stop', {
offset: Math.round( t * 100 ) + '%',
'stop-color': Utils.sampleColormap( t, mapName )
} ) );
}
}
}
Utils.buildColormapGradientEl = function ( gradId, mapName, x1, y1, x2, y2, useUserSpace ) {
var attrs = {
id: gradId,
x1: typeof x1 === 'number' ? x1.toFixed( 2 ) : ( x1 || '0%' ),
y1: typeof y1 === 'number' ? y1.toFixed( 2 ) : ( y1 || '0%' ),
x2: typeof x2 === 'number' ? x2.toFixed( 2 ) : ( x2 || '100%' ),
y2: typeof y2 === 'number' ? y2.toFixed( 2 ) : ( y2 || '0%' )
};
if ( useUserSpace ) {
attrs.gradientUnits = 'userSpaceOnUse';
}
var grad = Utils.createSvgEl( 'linearGradient', attrs );
appendGradientStops( grad, mapName );
return grad;
};
Utils.buildColormapRadialEl = function ( gradId, mapName, cx, cy, r ) {
var grad = Utils.createSvgEl( 'radialGradient', {
id: gradId,
cx: cx.toFixed( 2 ),
cy: cy.toFixed( 2 ),
r: r.toFixed( 2 ),
gradientUnits: 'userSpaceOnUse'
} );
appendGradientStops( grad, mapName );
return grad;
};
// =========================================================================
// 5. 折線路徑與幾何符號產生器 (buildPathD / createSymbolEl)
// =========================================================================
Utils.buildPathD = function ( pts, interpolate, isReverse ) {
var n = pts.length;
if ( n === 0 ) {
return '';
}
var list = isReverse ? pts.slice().reverse() : pts;
var mode = String( interpolate || 'linear' ).toLowerCase();
var d = [ ( isReverse ? 'L ' : 'M ' ) + list[ 0 ][ 0 ].toFixed( 2 ) + ',' + list[ 0 ][ 1 ].toFixed( 2 ) ];
if ( n === 1 ) {
return d[ 0 ];
}
if ( mode === 'step' ) {
for ( var i = 0; i < n - 1; i++ ) {
var midX = ( ( list[ i ][ 0 ] + list[ i + 1 ][ 0 ] ) / 2 ).toFixed( 2 );
d.push( 'H ' + midX + ' V ' + list[ i + 1 ][ 1 ].toFixed( 2 ) + ' H ' + list[ i + 1 ][ 0 ].toFixed( 2 ) );
}
} else if ( mode === 'step-after' ) {
for ( var sa = 0; sa < n - 1; sa++ ) {
d.push( 'H ' + list[ sa + 1 ][ 0 ].toFixed( 2 ) + ' V ' + list[ sa + 1 ][ 1 ].toFixed( 2 ) );
}
} else if ( mode === 'step-before' ) {
for ( var sb = 0; sb < n - 1; sb++ ) {
d.push( 'V ' + list[ sb + 1 ][ 1 ].toFixed( 2 ) + ' H ' + list[ sb + 1 ][ 0 ].toFixed( 2 ) );
}
} else if ( ( mode === 'monotone' || mode === 'cardinal' || mode === 'basis' || mode === 'bundle' ) && n >= 3 ) {
for ( var k = 0; k < n - 1; k++ ) {
var p0 = list[ Math.max( 0, k - 1 ) ];
var p1 = list[ k ];
var p2 = list[ k + 1 ];
var p3 = list[ Math.min( n - 1, k + 2 ) ];
var cp1x = p1[ 0 ] + ( p2[ 0 ] - p0[ 0 ] ) / 6;
var cp1y = p1[ 1 ] + ( p2[ 1 ] - p0[ 1 ] ) / 6;
var cp2x = p2[ 0 ] - ( p3[ 0 ] - p1[ 0 ] ) / 6;
var cp2y = p2[ 1 ] - ( p3[ 1 ] - p1[ 1 ] ) / 6;
if ( mode === 'monotone' ) {
var minY = Math.min( p1[ 1 ], p2[ 1 ] );
var maxY = Math.max( p1[ 1 ], p2[ 1 ] );
cp1y = Math.max( minY, Math.min( maxY, cp1y ) );
cp2y = Math.max( minY, Math.min( maxY, cp2y ) );
}
d.push(
'C ' + cp1x.toFixed( 2 ) + ',' + cp1y.toFixed( 2 ) + ' ' +
cp2x.toFixed( 2 ) + ',' + cp2y.toFixed( 2 ) + ' ' +
p2[ 0 ].toFixed( 2 ) + ',' + p2[ 1 ].toFixed( 2 )
);
}
} else {
for ( var j = 1; j < n; j++ ) {
d.push( 'L ' + list[ j ][ 0 ].toFixed( 2 ) + ',' + list[ j ][ 1 ].toFixed( 2 ) );
}
}
return d.join( ' ' );
};
Utils.createSymbolEl = function ( shapeKey, cx, cy, r, fill, stroke, strokeW, alpha ) {
var shape = Utils.SHAPES[ shapeKey ] || shapeKey || 'circle';
var attrs = { fill: fill || 'none' };
if ( strokeW && strokeW > 0 ) {
attrs.stroke = stroke;
attrs[ 'stroke-width' ] = strokeW.toFixed( 1 );
}
if ( alpha !== undefined && alpha < 1 ) {
attrs[ 'fill-opacity' ] = alpha.toFixed( 2 );
attrs[ 'stroke-opacity' ] = alpha.toFixed( 2 );
}
if ( shape === 'square' ) {
var half = r * 0.9;
attrs.x = ( cx - half ).toFixed( 2 );
attrs.y = ( cy - half ).toFixed( 2 );
attrs.width = ( half * 2 ).toFixed( 2 );
attrs.height = ( half * 2 ).toFixed( 2 );
return Utils.createSvgEl( 'rect', attrs );
} else if ( shape === 'diamond' ) {
var d = r * 1.3;
attrs.points = [
cx.toFixed( 2 ) + ',' + ( cy - d ).toFixed( 2 ),
( cx + d ).toFixed( 2 ) + ',' + cy.toFixed( 2 ),
cx.toFixed( 2 ) + ',' + ( cy + d ).toFixed( 2 ),
( cx - d ).toFixed( 2 ) + ',' + cy.toFixed( 2 )
].join( ' ' );
return Utils.createSvgEl( 'polygon', attrs );
} else if ( shape === 'triangle-up' ) {
var du = r * 1.25;
attrs.points = [
cx.toFixed( 2 ) + ',' + ( cy - du ).toFixed( 2 ),
( cx + du ).toFixed( 2 ) + ',' + ( cy + du * 0.75 ).toFixed( 2 ),
( cx - du ).toFixed( 2 ) + ',' + ( cy + du * 0.75 ).toFixed( 2 )
].join( ' ' );
return Utils.createSvgEl( 'polygon', attrs );
} else if ( shape === 'triangle-down' ) {
var dd = r * 1.25;
attrs.points = [
cx.toFixed( 2 ) + ',' + ( cy + dd ).toFixed( 2 ),
( cx + dd ).toFixed( 2 ) + ',' + ( cy - dd * 0.75 ).toFixed( 2 ),
( cx - dd ).toFixed( 2 ) + ',' + ( cy - dd * 0.75 ).toFixed( 2 )
].join( ' ' );
return Utils.createSvgEl( 'polygon', attrs );
} else if ( shape === 'triangle-right' ) {
var dr = r * 1.25;
attrs.points = [
( cx + dr ).toFixed( 2 ) + ',' + cy.toFixed( 2 ),
( cx - dr * 0.75 ).toFixed( 2 ) + ',' + ( cy - dr ).toFixed( 2 ),
( cx - dr * 0.75 ).toFixed( 2 ) + ',' + ( cy + dr ).toFixed( 2 )
].join( ' ' );
return Utils.createSvgEl( 'polygon', attrs );
} else if ( shape === 'triangle-left' ) {
var dl = r * 1.25;
attrs.points = [
( cx - dl ).toFixed( 2 ) + ',' + cy.toFixed( 2 ),
( cx + dl * 0.75 ).toFixed( 2 ) + ',' + ( cy - dl ).toFixed( 2 ),
( cx + dl * 0.75 ).toFixed( 2 ) + ',' + ( cy + dl ).toFixed( 2 )
].join( ' ' );
return Utils.createSvgEl( 'polygon', attrs );
} else if ( shape === 'cross' ) {
var dc = r * 0.95;
return Utils.createSvgEl( 'path', {
d: 'M ' + ( cx - dc ).toFixed( 2 ) + ',' + cy.toFixed( 2 ) + ' H ' + ( cx + dc ).toFixed( 2 ) +
' M ' + cx.toFixed( 2 ) + ',' + ( cy - dc ).toFixed( 2 ) + ' V ' + ( cy + dc ).toFixed( 2 ),
stroke: ( strokeW && strokeW > 0 ) ? stroke : ( fill !== 'none' ? fill : '#1f77b4' ),
'stroke-width': ( strokeW && strokeW > 0 ) ? strokeW : 2.0,
'stroke-linecap': 'round'
} );
} else if ( typeof shape === 'string' && shape !== 'circle' ) {
attrs.d = shape;
attrs.transform = 'translate(' + cx.toFixed( 2 ) + ',' + cy.toFixed( 2 ) + ') scale(' + ( r * 2.2 ).toFixed( 2 ) + ')';
return Utils.createSvgEl( 'path', attrs );
}
attrs.cx = cx.toFixed( 2 );
attrs.cy = cy.toFixed( 2 );
attrs.r = r.toFixed( 2 );
return Utils.createSvgEl( 'circle', attrs );
};
// 建立共用浮動 Tooltip 元件
Utils.createTooltipUI = function () {
var tipEl = Utils.createHtmlEl(
'div',
'gr-async-tooltip',
'display:none;position:absolute;z-index:20;pointer-events:none;background:rgba(255,255,255,0.96);' +
'border:1px solid #a2a9b1;border-radius:4px;padding:4px 8px;font-size:12px;color:#202122;' +
'box-shadow:0 2px 6px rgba(0,0,0,0.15);white-space:nowrap;transform:translate(-50%,-125%)'
);
var swatchEl = Utils.createHtmlEl(
'span',
'',
'display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle'
);
var titleEl = Utils.createHtmlEl( 'strong', '', 'margin-right:4px' );
var valEl = Utils.createHtmlEl( 'span', '' );
tipEl.appendChild( swatchEl );
tipEl.appendChild( titleEl );
tipEl.appendChild( valEl );
return {
el: tipEl,
show: function ( px, py, totalW, totalH, color, labelText, valueText ) {
if ( color ) {
swatchEl.style.display = 'inline-block';
swatchEl.style.backgroundColor = color;
} else {
swatchEl.style.display = 'none';
}
titleEl.textContent = labelText ? ( labelText + ( valueText ? ': ' : '' ) ) : '';
valEl.textContent = valueText || '';
var leftPct = ( px / Math.max( 1, totalW ) ) * 100;
var topPct = ( py / Math.max( 1, totalH ) ) * 100;
tipEl.style.left = leftPct.toFixed( 2 ) + '%';
tipEl.style.top = topPct.toFixed( 2 ) + '%';
var tx = leftPct < 20 ? '0%' : ( leftPct > 80 ? '-100%' : '-50%' );
var ty = topPct < 25 ? '25%' : '-125%';
tipEl.style.transform = 'translate(' + tx + ',' + ty + ')';
tipEl.style.display = 'block';
},
hide: function () {
tipEl.style.display = 'none';
}
};
};
GW.Utils = Utils;
}() );