Motivation: Visualize 2 Dimensional Data with Lightweight Way
I wrote small java program for calculating Jaccard Similarity among source code in this post.
And map into them 2 dimensional map using Multi Dimensional Scaling technique. I have used MDSJ – Multidimensional Scaling for Java.
(By the way the library is quite simple and meet my requirement perfectly!! Excellent!!)>
Next step is how to visualize the bunch of 2 dimensional (x,y) coordinate data so that human can easily understand.
Of course, I am good at Java, I can write some code for visualize data in Java using Swing or Java FX etc.
But it's not lightweight way.
So, I decided to use HTML5 Canvas + Javascript.
Viewer: Html5 Canvas + Javascript
The features of the Data Viewer are:
- Visualize CSV data
- Expected 3 columns without header row: name, x coordinate, y coordinate
- Data will be fitted into the graph area by calculating min, max value and normalize the original data when visualize them
- If you mouse over the data point, data name which is in the original source csv data will be displayed on right side of the canvas
- You can control are for picking up data points by mouse wheel
The screenshot of the visualizing result is below.
Demo
Please try the below visualizer demo yourself.
In this demo, I made the font size of the right div area, which displays data point name, smaller intentionally in order to fit the right div area in the blog post area.
Expect csv format: name,x,y - e.g. "point A", 1.3, 4.5
Source Code
Please feel free to use and customize the source code so that it fits to your data visualization purpose.
I think the code itself is not so complicated... Hope this code helps your data visualization task ;)
<div style="display: inline-block; vertical-align: top; max-height: 1024px; border:1px solid #000000;"> <div style="margin: auto;"> <canvas id="myCanvas"></canvas> </div> <input type="file" id="file" name="file"/><br> Expect csv format: name,x,y - e.g. "point A", 1.3, 4.5 </div> <div id="pointList" style=" display: inline-block; vertical-align: top;"> </div> <script> var canvasWidth = 1280; var canvasHeight = 1024; var scale = 0.8; var rangeX = 1; var rangeY = 1; var xmax = 0; var minX = 0; var ymax = 0; var minY = 0; var p = []; var mouseR = 40; var canvas = document.getElementById("myCanvas"); canvas.width = canvasWidth; canvas.height = canvasHeight; var ctx = canvas.getContext("2d"); drawBackGround(ctx); function drawBackGround(ctx) { ctx.beginPath(); ctx.rect(0, 0, canvasWidth, canvasHeight); ctx.fillStyle = 'white'; ctx.fill(); } function drawPoint(x, y, size) { var offset = size / 2; ctx.fillRect(x - offset, y - offset, size, size); } function drawPoints() { for (var i = 0; i < p.length; i++) { drawPoint( toCanvasCoordinates(p[i].x, minX, rangeX, canvasWidth), toCanvasCoordinates(p[i].y, minY, rangeY, canvasHeight), 4 ); } } // we can improve performance by pre calculating "scale * canvasRange" or "canvasRange*(1-scale)/2" // but write as below for code simplicity function toCanvasCoordinates(v, min, range, canvasRange) { return scale * canvasRange * (v-min)/range + canvasRange*(1-scale)/2; } function toActualCoordinates(v, min, range, canvasRange) { return (v-canvasRange*(1-scale)/2) * range / (canvasRange * scale) + min; } var myimage = document.getElementById("myCanvas"); if (myimage.addEventListener) { // IE9, Chrome, Safari, Opera myimage.addEventListener("mousewheel", MouseWheelHandler, false); // Firefox myimage.addEventListener("DOMMouseScroll", MouseWheelHandler, false); } // IE 6/7/8 else myimage.attachEvent("onmousewheel", MouseWheelHandler); function MouseWheelHandler(e) { e.preventDefault(); // cross-browser wheel delta var e = window.event || e; // old IE support var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))); mouseR+=delta; paint(e); } canvas.addEventListener('mousemove', function (e) { paint(e); }, false); function getMousePos(canvas, evt) { var rect = canvas.getBoundingClientRect(); return { x: evt.clientX - rect.left, y: evt.clientY - rect.top }; } function paint(e) { drawBackGround(ctx); ctx.fillStyle = "#FF0000"; drawPoints(); ctx.strokeStyle = 'black'; var mousePos = getMousePos(canvas, e); ctx.beginPath(); ctx.arc(mousePos.x, mousePos.y, mouseR, 0, 2 * Math.PI); ctx.stroke(); var pointInfoDiv = document.getElementById("pointList"); pointInfoDiv.innerHTML = '<ul></ul>'; var pointInfoInnerHtml = []; var mx = toActualCoordinates(mousePos.x, minX, rangeX, canvasWidth); var my = toActualCoordinates(mousePos.y, minY, rangeY, canvasHeight); var mr2 = mouseR * mouseR * rangeX * rangeY / (canvasWidth * canvasHeight); for (var i = 0; i < p.length; i++) { if ((p[i].x - mx) * (p[i].x - mx) + (p[i].y - my) * (p[i].y - my) <= mr2) { pointInfoInnerHtml.push('<li>' + p[i].name + '</li>'); } } if (pointInfoInnerHtml.length > 0) { pointInfoInnerHtml.sort(); pointInfoDiv.innerHTML = '<ul>' + pointInfoInnerHtml.join('') + '</ul>'; } } // Check for the various File API support. if (window.File && window.FileReader && window.FileList && window.Blob) { // Great success! All the File APIs are supported. } else { alert('The File APIs are not fully supported in this browser.'); } function handleFileSelect(evt) { var files = evt.target.files; // FileList object // files is a FileList of File objects. List some properties. for (var i = 0; i < files.length; i++) { var f = files[i]; var r = new FileReader(); r.onload = function (e) { var text = r.result; var points = parseCSV(text); p = []; for (var i = 0; i < points.length; i++) { var x = parseFloat(points[i][1]); var y = parseFloat(points[i][2]); p.push({x: x, y: y, name: points[i][0].replace(/.*\\(.+)\..+/, "$1")}); if (x > xmax) xmax = x; if (x < minX) minX = x; if (y > ymax) ymax = y; if (y < minY) minY = y; } rangeX = xmax - minX; rangeY = ymax - minY; ctx.fillStyle = "#FF0000"; drawPoints(); }; r.readAsText(f, 'utf-8'); } } document.getElementById('file').addEventListener('change', handleFileSelect, false); // see http://stackoverflow.com/questions/1293147/javascript-code-to-parse-csv-data function parseCSV(str) { var arr = []; var quote = false; // true means we're inside a quoted field // iterate over each character, keep track of current row and column (of the returned array) for (var row = col = c = 0; c < str.length; c++) { var cc = str[c], nc = str[c + 1]; // current character, next character arr[row] = arr[row] || []; // create a new row if necessary arr[row][col] = arr[row][col] || ''; // create a new column (start with empty string) if necessary // If the current character is a quotation mark, and we're inside a // quoted field, and the next character is also a quotation mark, // add a quotation mark to the current column and skip the next character if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; } // If it's just one quotation mark, begin/end quoted field if (cc == '"') { quote = !quote; continue; } // If it's a comma and we're not in a quoted field, move on to the next column if (cc == ',' && !quote) { ++col; continue; } // If it's a newline and we're not in a quoted field, move on to the next // row and move to column 0 of that new row if (cc == '\n' && !quote) { ++row; col = 0; continue; } // Otherwise, append the current character to the current column arr[row][col] += cc; } return arr; } </script>
コメント