/////////////////////////////////////////////////////////////////////////////// /* graph.js Using Scalable Vector Graphics (SVG) generate the required graphics. COPYRIGHT --------- Copyright (C) 2014-2023 Mark G.Daniel This program, comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version. http://www.gnu.org/licenses/gpl.txt VERSION ------- 22-APR-2014 MGD initial */ /////////////////////////////////////////////////////////////////////////////// /* Generate the bar and history (even if not visible) graphs of the system "dynamic" data. */ function mdsiGraphDynamic() { if (!(mdsi_StaticData && mdsi_DynamicData)) return; mdsiDataAveMax ('cpuBusy'); mdsiDataAveMax ('memInUse'); mdsiDataAveMax ('pageFileUsed'); mdsiDataAveMax ('pageFaults'); mdsiDataAveMax ('bio'); mdsiDataAveMax ('dio'); mdsiDataAveMax ('lockActivity'); mdsiDataAveMax ('netPerSec'); mdsiDataAveMax ('fcpRW'); mdsiDataAveMax ('procTot'); if (mdsi_StaticData.clusterMember) { mdsiDataAveMax ('clusterMsgTotal'); mdsiDataAveMax ('clusterKbyteMap'); mdsiDataAveMax ('mscpTotal'); } mdsiDisplayModes(); mdsiDisplaySummary(); mdsiDisplaySynopsis(); mdsiBarGraph ('cpuBar', 'CPU', 'cpuBusy', null, 'cpuSEK'); mdsiBarGraph ('memBar', 'MEM', 'memInUse'); mdsiBarGraph ('fltsBar', 'FLTS', 'pageFaults', null, 'pageFaultsHard'); mdsiBarGraph ('bioBar', 'BIO', 'bio'); mdsiBarGraph ('dioBar', 'DIO', 'dio'); mdsiBarGraph ('lockBar', 'LOCK', 'lockActivity', 'lockWait'); mdsiBarGraph ('netBar', 'NET', 'netPerSec', 'netRxPerSec'); mdsiBarGraph ('diskBar', 'DISK', 'fcpRW', null, 'fcpWrite'); mdsiBarGraph ('procBar', 'PROC', 'procTot', 'procInt'); if (mdsi_StaticData.clusterMember) { mdsiBarGraph ('scsmsgBar', 'SCSMSG', 'clusterMsgTotal', 'clusterMsgSent'); mdsiBarGraph ('scskbBar', 'SCSKB', 'clusterKbyteMap'); mdsiBarGraph ('mscpBar', 'MSCP', 'mscpTotal', 'mscpMsgSent'); } mdsiHistoryGraph ('cpuHist', 'CPU', 'cpuBusy', null, 'cpuSEK'); mdsiModesGraph ('cpuModesHist', 'CPU'); mdsiHistoryGraph ('memHist', 'MEM \u21b7', 'memInUse'); mdsiHistoryGraph ('pgflHist', 'PGFL \u21b7', 'pageFileUsed'); mdsiHistoryGraph ('fltsHist', 'FLTS', 'pageFaults', null, 'pageFaultsHard'); mdsiHistoryGraph ('bioHist', 'BIO', 'bio'); mdsiHistoryGraph ('dioHist', 'DIO', 'dio'); mdsiHistoryGraph ('lockHist', 'LOCK', 'lockActivity', null, 'lockWait'); var NIlabel = 'NET \u21b7'; var err = mdsiGetData('netErrors'); if (err > 0) NIlabel += '\n!ERR: ' + err; mdsiHistoryGraph ('netHist', NIlabel, 'netPerSec', 'netRxPerSec'); mdsiHistoryGraph ('netDgramHist', 'DGRAM \u21b7', 'netDgramTx', 'netDgramRx'); mdsiHistoryGraph ('diskHist', 'DISK', 'fcpRW', null, 'fcpWrite'); mdsiHistoryGraph ('procHist', 'PROC', 'procTot', 'procInt'); if (mdsi_StaticData.clusterMember) { mdsiHistoryGraph ('scsmsgHist', 'SCSMSG', 'clusterMsgTotal', 'clusterMsgSent'); mdsiHistoryGraph ('scskbHist', 'SCSKB', 'clusterKbyteMap'); mdsiHistoryGraph ('mscpHist', 'MSCP', 'mscpTotal', 'mscpWrite'); } } /////////////////////////////////////////////////////////////////////////////// /* Generate disk graphs. */ function mdsiGraphDisk() { var cnt = mdsiGetData('diskCount'); for (var idx = 0; idx < cnt; idx++) { var disk = 'disk' + idx; mdsiDataAveMax (disk+'iops'); mdsiDataAveMax (disk+'qlen'); mdsiDataAveMax (disk+'delval'); mdsi_DataMaximum[disk+'delval'] = mdsiGetData(disk+'delta'); mdsi_DataMaximum[disk+'qlen'] = mdsiGetData(disk+'qmax'); } for (var idx = 0; idx < cnt; idx++) { var disk = 'disk' + idx; var delta = mdsiGetData(disk+'delta'); var delsym = mdsiGetData(disk+'delsym'); var dev = mdsiGetData(disk+'dev'); var err = mdsiGetData(disk+'err'); var free = mdsiGetData(disk+'free'); var iops = mdsiGetData(disk+'iops'); var size = mdsiGetData(disk+'size'); var mvip = mdsiGetData(disk+'mvip'); var qlen = mdsiGetData(disk+'qlen'); var qmax = mdsiGetData(disk+'qmax'); var vol = mdsiGetData(disk+'vol'); if (vol.length) dev += ' ' + vol; var bardev = dev; if (vol.length) bardev += ' \u0394' + mdsiValue(delta,0) + delsym + ' \u2630' + qlen + '/' + qmax; mdsiBarGraph ('perDiskBar'+idx, bardev, disk+'iops'); if (vol.length) { if (!(mdsi_DiskDeltaDisplay || mdsi_DiskQlenDisplay)) dev += ' \u21b7'; dev += '\n' + mdsiValue(size) + 'B ' + (free * 100 / size).toFixed(0) + '%' + ' \u0394' + mdsiValue(delta,0) + mdsiGetData(disk+'delsym'); dev += '\n\u2630' + qlen + '/' + qmax; } if (err > 0) dev += '\n!ERR: ' + err; if (mvip) { if (err == 0) dev += '\n!\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0'; dev += '\u00a0\u00a0\u00a0*** MOUNT VERIFY ***'; } mdsiHistoryGraph ('perDiskHistIOPs'+idx, dev, disk+'iops'); if (vol.length) { dev = mdsiGetData(disk+'dev') +' ' + vol; if (!(mdsi_DiskDeltaDisplay || mdsi_DiskQlenDisplay)) dev += ' \u21b7\n' + mdsiValue(size) + 'B ' + (free * 100 / size).toFixed(0) + '%'; } if (!mdsi_DiskDeltaDisplay && err > 0) dev += '\n!ERR: ' + err; if (mvip) { if (mdsi_DiskDeltaDisplay || err == 0) dev += '\n!\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0'; dev += '\u00a0\u00a0\u00a0*** MOUNT VERIFY ***'; } mdsiHistoryGraph ('perDiskHistQlen'+idx, dev, disk+'qlen', disk); mdsiHistoryGraph ('perDiskHistDelta'+idx, dev, disk+'delval', disk); } } /////////////////////////////////////////////////////////////////////////////// /* Generate the per-process history graph (irrespective of visibility). */ function mdsiGraphPerProcess() { if (!mdsi_PerProcessPid) return; var pid = mdsi_PerProcessPid; mdsiDataAveMax (pid+'CPU'); mdsiDataAveMax (pid+'BufIO'); mdsiDataAveMax (pid+'DirIO'); mdsiDataAveMax (pid+'PageFaults'); mdsiDataAveMax (pid+'PageFileUsed'); mdsiDataAveMax (pid+'PrivatePages'); mdsiDataAveMax (pid+'WorkingSet'); mdsiHistoryGraph ('perCpuHist', 'CPU\u2020', pid+'CPU', null, pid+'SEK'); mdsiHistoryGraph ('perWsetHist', 'WSET\u2020 \u21b7', pid+'WorkingExtent', pid+'WorkingSet'); mdsiHistoryGraph ('perPgflHist', 'PGFL\u2020', pid+'PageFileQuota', pid+'PageFileUsed'); mdsiHistoryGraph ('perPgsHist', 'PGS\u2020', pid+'PrivatePages', pid+'GlobalPages'); mdsiHistoryGraph ('perBioHist', 'BIO\u2020', pid+'BufIO'); mdsiHistoryGraph ('perDioHist', 'DIO\u2020', pid+'DirIO'); mdsiHistoryGraph ('perFltsHist', 'FLTS\u2020', pid+'PageFaults'); } /////////////////////////////////////////////////////////////////////////////// /* */ var mdsi_HistoryHeight = mdsi_HistHeight, mdsi_HistoryWidth = mdsi_HistWidth5; function mdsiSetHistoryWidth (seconds) { mdsiCSS ('.history', 'width', seconds+'px'); mdsiCSS ('.diskory', 'width', seconds+'px'); mdsi_HistoryWidth = seconds; mdsi_ModesWidth = seconds; mdsiStoreSeconds (seconds); } function mdsiSetHistoryHeight (height) { mdsiCSS ('.history', 'height', height+'px'); mdsiCSS ('.diskory', 'height', height+'px'); mdsi_HistoryHeight = height; mdsi_ModesHeight = height; } /////////////////////////////////////////////////////////////////////////////// /* Scan all data in the (dynamic) DataStore for the given datum. Calculate maximum and average values and store in the respective arrays. */ var mdsi_DataAverage = [], mdsi_DataMaximum = [], mdsi_DataMinimum = []; function mdsiDataAveMax (datum) { var val, ave = 0, cnt = 0, len = mdsiStoreSize()-1, max = 0, min = 9007199254740992; for (var idx = 0; idx < len; idx++) { cnt++; val = mdsiGetData(datum,null,idx); ave += val; if (val > max) max = val; if (val < min) min = val; } if (cnt) mdsi_DataAverage[datum] = ave / cnt; mdsi_DataMaximum[datum] = max; mdsi_DataMinimum[datum] = min; } /////////////////////////////////////////////////////////////////////////////// /* Generate the instantaneous data display bar graph. The function parameters can take one to three additional arguments. These are the names of each of the data. The first is plotted in green, the second in blue, the third in red. Any may be omitted except the first. */ var mdsi_BarClass = []; mdsi_BarClass [0] = 'bar_red'; mdsi_BarClass [1] = 'bar_green'; mdsi_BarClass [2] = 'bar_blue'; mdsi_BarClass [3] = 'bar_red'; function mdsiBarGraph (name,title) { if (!mdsiStoreSize()) return; var ave, color, dcnt, max, plot, string, val; var datum = []; var id = $byId(name); // datum has [1]..[n] from arguments [2]..[n] for (dcnt = 1; dcnt <= 3 && dcnt <= arguments.length-2; dcnt++) datum[dcnt] = arguments[dcnt+1]; /////////////////// // special cases // /////////////////// if (name == 'cpuBar') { var cpuActive = mdsiGetData('cpuActive'); if (cpuActive > 1) { max = 100 * cpuActive; title = 'CPUx' + cpuActive; } else max = 100; ave = mdsi_DataAverage[datum[1]]; } else if (name == 'memBar') { max = mdsi_StaticData.memSize; ave = mdsi_DataAverage['memInUse']; } else if (name.substr(0,4) == 'mode') { var cpuActive = mdsiGetData('cpuActive'); if (cpuActive > 1) max = 100 * cpuActive; else max = 100; ave = mdsi_DataAverage[datum[1]]; } else { max = mdsi_DataMaximum[datum[1]]; ave = mdsi_DataAverage[datum[1]]; } /////////////////// // generate bars // /////////////////// var svg = ''; string = mdsiValue(max); svg += '' + string + ''; if (ave) { if (ave < 0) ave = 0; if (ave > max) ave = max; plot = 0; if (max) plot = ave / max * mdsi_BarWidth; svg += ''; if (plot > mdsi_BarLabelWidth) { if (name.substr(0,10) != 'perDiskBar') { string = mdsiValue(ave); svg += '' + string + ''; } } } for (dcnt = 1; dcnt <= 3; dcnt++) { if (!datum[dcnt]) continue; val = mdsiGetData(datum[dcnt]); if (val < 0) val = 0; if (val > max) val = max; plot = 0; if (max) plot = val / max * mdsi_BarWidth; if (name.substr(0,4) != 'mode' || name.substr(0,5) == 'modeu') color = mdsi_BarClass[dcnt]; else color = mdsi_BarClass[0]; svg += ''; if (plot > mdsi_BarLabelWidth) { if (val >= max || name.substr(0,10) != 'perDiskBar') { string = mdsiValue(val); svg += '' + string + ''; } } } svg += '' + title + ''; svg += ''; id.innerHTML = svg; } /////////////////////////////////////////////////////////////////////////////// /* Generate and display the history (line) graph. The function parameters can take one to three additional arguments. These are the names of each of the data. The first is plotted in green, the second in blue, the third in red. Any may be omitted except the first. */ var mdsi_HistoryClass = []; mdsi_HistoryClass [1] = ['hist_green','hist_text_green']; mdsi_HistoryClass [2] = ['hist_blue','hist_text_blue']; mdsi_HistoryClass [3] = ['hist_red','hist_text_red']; function mdsiHistoryGraph (name,title) { if (!mdsiStoreSize()) return; var ave, dcnt, dscnt, lastXnone, max, string, tcnt, val; var datum = [], points = [], valdcnt = []; var xpos = mdsi_HistoryWidth, lastXpos = mdsi_HistoryWidth; var id = $byId(name); // datum has [1]..[n] from arguments [2]..[n] for (dcnt = 1; dcnt <= 3 && dcnt <= arguments.length-2; dcnt++) { datum[dcnt] = arguments[dcnt+1]; points[dcnt] = ''; } var interval = mdsi_StaticData.interval; var timestamp = mdsiGetData('timestamp'); /////////////////// // special cases // /////////////////// if (name == 'cpuHist' || name == 'perCpuHist') { var cpuActive = mdsiGetData('cpuActive'); if (cpuActive > 1) { max = 100 * cpuActive; title = 'CPUx' + cpuActive; } else max = 100; ave = mdsi_DataAverage[datum[1]]; } else if (name == 'memHist') { max = mdsi_StaticData.memSize; ave = mdsi_DataAverage['memInUse']; } else if (name == 'pgflHist') { max = mdsiGetData('pageFileSize'); ave = mdsi_DataAverage['pageFileUsed']; } else if (name == 'perWsetHist') { max = mdsiGetData(datum[1]); ave = mdsi_DataAverage[datum[2]]; // bit shonky here datum[1] = datum[2]; datum[2] = datum[3]; datum[3] = null; } else if (name == 'perPgflHist') { max = mdsiGetData(datum[1]); ave = mdsi_DataAverage[datum[2]]; // bit shonky here datum[1] = datum[2]; datum[2] = null; datum[3] = null; } else if (name == 'netDgramHist') { if (mdsiGetData(datum[1]) > mdsiGetData(datum[2])) { mdsiDataAveMax (datum[1]); max = mdsi_DataMaximum[datum[1]]; ave = mdsi_DataAverage[datum[1]]; } else { mdsiDataAveMax (datum[2]); max = mdsi_DataMaximum[datum[2]]; ave = mdsi_DataAverage[datum[2]]; } } else { max = mdsi_DataMaximum[datum[1]]; ave = mdsi_DataAverage[datum[1]]; } ////////////////////////// // generate data points // ////////////////////////// var svg = ''; dscnt = lastXnone = 0; for (var idx = mdsiStoreSize()-1; idx > 0; idx--) { dscnt++; var step = timestamp - mdsiGetData('timestamp',null,idx); if ((xpos -= step) < 0) break; timestamp = mdsiGetData('timestamp',null,idx); // paper-over reasonable data gaps from XHR re-requests var gapInterval = $WebSocket ? interval+1 : interval*3; if (step > gapInterval) { /////////////// // data loss // /////////////// svg += '' + ''; } if (mdsi_PerProcessPid && mdsi_PerProcessPid == datum[1].substr(0,8)) { ////////////////////// // per-process data // ////////////////////// if (mdsiGetData(datum[1],null,idx,null) == null) { lastXnone = lastXpos; continue; } if (lastXnone) { ////////////// // data gap // ////////////// svg += '' + ''; } } ////////// // data // ////////// // for each of the three possible data for (dcnt = 1; dcnt <= 3; dcnt++) { if (!datum[dcnt]) continue; var cmd = ' L'; if (!points[dcnt].length || step > gapInterval || lastXnone) cmd = ' M'; val = mdsiGetData(datum[dcnt],null,idx); if (max) val = (mdsi_HistoryHeight / max * val).toFixed(0); else val = 0; points[dcnt] += cmd + xpos + ',' + (mdsi_HistoryHeight - val); valdcnt[dcnt] = val; } lastXpos = xpos; lastXnone = 0; } ////////////////// // plot average // ////////////////// if (dscnt > 10) { if (ave > 1) { var aveplot = 0; if (max) aveplot = mdsi_HistoryHeight - (mdsi_HistoryHeight / max * ave); svg += ''; if (aveplot > 15 && aveplot < mdsi_HistoryHeight - 15) svg += '' + mdsiValue(ave) + ''; } } ////////////////////// // plot decorations // ////////////////////// // if least recent data not plotted then draw virtual edge for stored data if (lastXpos > interval * 2) svg += ''; // minute graduations along the X axis for (xpos = mdsi_HistoryWidth-60; xpos > 0; xpos -= 60) svg += ''; /////////////// // plot data // /////////////// if (name.substr(0,16) == 'perDiskHistDelta') string = '\u0394' + mdsiValue(max,0) + mdsiGetData(datum[2]+'delsym'); else if (name.substr(0,15) == 'perDiskHistQlen') string = '\u2630' + mdsiGetData(datum[2]+'qmax'); else string = mdsiValue(max); svg += '' + string + ''; tcnt = 0; for (dcnt = 1; dcnt <= 3; dcnt++) { if (!datum[dcnt]) continue; tcnt++; if (points[dcnt].length) svg += ''; if ((val = mdsiGetData(datum[dcnt]))) { if (name.substr(0,16) == 'perDiskHistDelta') string = mdsiValue(val,0); else string = mdsiValue(val); svg += '' + string + ''; } } //////////////////// // title of graph // //////////////////// title = title.split('\n'); svg += '' + title[0] + ''; if (typeof title[1] != 'undefined') { var clss = 'hist_text'; if (title[1].substr(0,1) == '!') { title[1] = title[1].substr(1); clss += '_red'; } svg += '' + title[1] + ''; } if (typeof title[2] != 'undefined') { var clss = 'hist_text'; if (title[2].substr(0,1) == '!') { title[2] = title[2].substr(1); clss += '_red'; } svg += '' + title[2] + ''; } svg += ''; id.innerHTML = svg; } /////////////////////////////////////////////////////////////////////////////// /* Generate and display the history (line) graph. The function parameters can take one to three additional arguments. These are the names of each of the data. The first is plotted in green, the second in blue, the third in red. Any may be omitted except the first. */ var mdsi_ModesNames = ['','cpuBusy','modeKER','modeEXE','modeSUP','modeUSE', 'modeINT','modeMPS']; var mdsi_ModesCount = mdsi_ModesNames.length - 1; var mdsi_ModesWidth = mdsi_HistoryWidth, mdsi_ModesHeight = mdsi_HistoryHeight; var mdsi_ModesClass = []; mdsi_ModesClass [1] = ['modes_total','modes_text_total']; mdsi_ModesClass [2] = ['modes_kernel','modes_text_kernel']; mdsi_ModesClass [3] = ['modes_exec','modes_text_exec']; mdsi_ModesClass [4] = ['modes_super','modes_text_super']; mdsi_ModesClass [5] = ['modes_user','modes_text_user']; mdsi_ModesClass [6] = ['modes_inter','modes_text_inter']; mdsi_ModesClass [7] = ['modes_mpsyn','modes_text_mpsyn']; function mdsiModesGraph (name,title) { if (!mdsiStoreSize()) return; var dcnt, dscnt, lastXnone, max, string, tcnt, val, x2; var datum = [], points = [], valdcnt = []; var xpos = mdsi_ModesWidth, lastXpos = mdsi_ModesWidth; var id = $byId(name); for (dcnt = 1; dcnt <= mdsi_ModesCount; dcnt++) { datum[dcnt] = mdsiGetData(mdsi_ModesNames[dcnt]); points[dcnt] = ''; } var interval = mdsi_StaticData.interval; var timestamp = mdsiGetData('timestamp'); var cpuActive = mdsiGetData('cpuActive'); if (cpuActive > 1) { max = 100 * cpuActive; title = 'CPUx' + cpuActive; } else max = 100; ////////////////////////// // generate data points // ////////////////////////// var svg = ''; dscnt = lastXnone = 0; for (var idx = mdsiStoreSize()-1; idx > 0; idx--) { dscnt++; var step = timestamp - mdsiGetData('timestamp',null,idx); if ((xpos -= step) < 0) break; timestamp = mdsiGetData('timestamp',null,idx); // paper-over reasonable data gaps from XHR re-requests var gapInterval = $WebSocket ? interval+1 : interval*3; if (step > gapInterval) { /////////////// // data loss // /////////////// svg += '' + ''; } ////////// // data // ////////// /* for each of seven possible modes */ for (dcnt = 1; dcnt <= mdsi_ModesCount; dcnt++) { val = mdsiGetData(mdsi_ModesNames[dcnt],null,idx); var cmd = ' L'; if (!points[dcnt].length || step > gapInterval || lastXnone) cmd = ' M'; if (max) val = (mdsi_ModesHeight / max * val).toFixed(0); else val = 0; points[dcnt] += cmd + xpos + ',' + (mdsi_ModesHeight - val); valdcnt[dcnt] = val; } lastXpos = xpos; lastXnone = 0; } ////////////////////// // plot decorations // ////////////////////// // if least recent data not plotted then draw virtual edge for stored data if (lastXpos > interval * 2) svg += ''; // minute graduations along the X axis for (xpos = mdsi_ModesWidth-60; xpos > 0; xpos -= 60) svg += ''; /////////////// // plot data // /////////////// string = mdsiValue(max); svg += '' + string + ''; tcnt = 0; x2 = 1; // x2 = mdsi_ModesHeight / 100; for (dcnt = 1; dcnt <= mdsi_ModesCount; dcnt++) { tcnt++; if (points[dcnt].length) svg += ''; // mode values along Y axis string = mdsiValue(mdsiGetData(mdsi_ModesNames[dcnt])); svg += '' + string + ''; } //////////////////// // title of graph // //////////////////// title = title.split('\n'); svg += '' + title[0] + ''; if (typeof title[1] != 'undefined') { var clss = 'modes_text'; if (title[1].substr(0,1) == '!') { title[1] = title[1].substr(1); clss += '_kernel'; } svg += '' + title[1] + ''; } if (typeof title[2] != 'undefined') { var clss = 'modes_text'; if (title[2].substr(0,1) == '!') { title[2] = title[2].substr(1); clss += '_kernel'; } svg += '' + title[2] + ''; } svg += ''; id.innerHTML = svg; } /////////////////////////////////////////////////////////////////////////////// /* Return a string respresenting the value paramater transmogrified to kilos and megas as seem more meaningful. Kilo == 1024! */ function mdsiValue (value,kat) { // useful during development :-) if (typeof value == 'undefined') return '?'; if (typeof kat == 'undefined') kat = 10; var fixed = 0; if (value > 1099511627776) { if (value > 1099511627776*100) fixed = 1; else if (value > 1099511627776*10) fixed = 2; else fixed = 3; var string = ((value/1099511627776).toFixed(fixed)-0).toString() + 'T'; } else if (value > 1073741824) { if (value > 1073741824*100) fixed = 1; else if (value > 1073741824*10) fixed = 2; else fixed = 3; var string = ((value/1073741824).toFixed(fixed)-0).toString() + 'G'; } else if (value > 1048576) { if (value > 1048576*100) fixed = 1; else if (value > 1048576*10) fixed = 2; else fixed = 3; var string = ((value/1048576).toFixed(fixed)-0).toString() + 'M'; } else if (value > 1024*kat) { if (value <= 1024*100) fixed = 1; var string = ((value/1024).toFixed(fixed)-0).toString() + 'k'; } else var string = value.toFixed(0); return string; } /////////////////////////////////////////////////////////////////////////////// function mdsiWithCommas(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, mdsi_ThousandComma); } /////////////////////////////////////////////////////////////////////////////// function mdsiPixelWidth(string) { var length = string.length; // whadda kludge! if (string.indexOf('\u2630') >= 0) length += 0.5; return (length * 7); } ///////////////////////////////////////////////////////////////////////////////