UNPKG

smoothie

Version:

Smoothie Charts: smooooooth JavaScript charts for realtime streaming data

626 lines (561 loc) 24.1 kB
<!DOCTYPE html> <html> <head> <script type="text/javascript" src="../smoothie.js"></script> <link href="http://fonts.googleapis.com/css?family=Pacifico" rel="stylesheet" type="text/css"> <script type="text/javascript"> var series = new TimeSeries(); // Add random data points at irregular intervals var maxYValue = 10000; var addValueLoop = function() { setTimeout(function() { // Generate a random value, centered around zero, raised to the power of 5 // so that larger values occur less frequently var value = Math.pow(Math.random() * 2 - 1, 5) * maxYValue; series.append(Date.now(), value); addValueLoop(); }, Math.random() * 500); }; addValueLoop(); // A function for nicely rounding numbers up for human beings. // Eg: 180.2 -> 200 // 3.5 -> 5 // 8.9 -> 10 var ln10 = Math.log(10); var roundUpHumane = function(value) { // calculate the magnitude of the value var mag = Math.floor(Math.log(value) / ln10); var magPow = Math.pow(10, mag); // calculate the most significant digit of the value var magMsd = Math.ceil(value / magPow); // promote the MSD to either 1, 2, or 5 if (magMsd > 5.0) magMsd = 10.0; else if (magMsd > 2.0) magMsd = 5.0; else if (magMsd > 1.0) magMsd = 2.0; return magMsd * magPow; }; var customYRangeFunction = function(range) { // Find the greatest absolute value var max = Math.max(Math.abs(range.min), Math.abs(range.max)); // Ensure we're viewing at least a quarter of the range, so that // very small values don't appear exaggeratedly large max = Math.max(max, 200); // Round the limit up to a more pleasant number max = roundUpHumane(max); return {min: -max, max: max}; }; var sampleHorizontalLines = [ {color:'#ffffff', lineWidth: 1, value: 0}, {color:'#880000', lineWidth: 2, value: Math.round(maxYValue/3)}, {color:'#880000', lineWidth: 2, value: Math.round(-maxYValue/3)} ]; var updateCode = function(canvas, chart) { document.getElementById('html-code').textContent = '<canvas id="smoothie-chart" width="' + canvas.width + '" height="' + canvas.height + '">'; var chartOptions = difference(chart.options, SmoothieChart.defaultChartOptions); var seriesOptionsJs = toJsCode(difference(chart.seriesSet[0].options, SmoothieChart.defaultSeriesPresentationOptions)); var js = ""; document.getElementById('css-code').style.display = chart.options.tooltip ? 'block' : 'none'; if (chart.options.timestampFormatter) { chartOptions.timestampFormatter = "SmoothieChart.timeFormatter"; } if (chart.options.yRangeFunction) { chartOptions.yRangeFunction = "myYRangeFunction"; js += "function myYRangeFunction(range) {\n"; js += " // TODO implement your calculation using range.min and range.max\n"; js += " var min = ...;\n"; js += " var max = ...;\n"; js += " return {min: min, max: max};\n"; js += "}\n\n"; } if (chart.options.horizontalLines && chart.options.horizontalLines.length) { chartOptions.horizontalLines = chart.options.horizontalLines; } js += "var chart = new SmoothieChart(" + toJsCode(chartOptions) + "),\n"; js += " canvas = document.getElementById('smoothie-chart'),\n"; js += " series = new TimeSeries();\n\n"; js += "chart.addTimeSeries(series" + (seriesOptionsJs ? ', ' + seriesOptionsJs : '') + ");\n"; js += "chart.streamTo(canvas, " + chart.delay + ");"; js = js.replace("'SmoothieChart.timeFormatter'", 'SmoothieChart.timeFormatter') .replace("'myYRangeFunction'", 'myYRangeFunction'); document.getElementById('javascript-code').textContent = js; }; var toJsCode = function(obj) { var code = JSON.stringify(obj).replace(/"([a-z0-9_]+)":/gi,'$1:').replace(/"/g, "'"); if (code === '{}') { code = ''; } return code; }; // returns values in a but not in b var difference = function(a, b) { var result = {}; for (var key in a) { if (!a.hasOwnProperty(key) || a[key] === b[key]) { continue; } if (typeof(a[key]) === 'object') { if (a[key] instanceof Array) { // ignore arrays } else { var sub = difference(a[key], b[key]); if (Object.keys(sub).length) result[key] = sub; } } else { result[key] = a[key]; } } return result; }; function onLoad() { var chart = new SmoothieChart(), canvas = document.getElementById('chart'); chart.addTimeSeries(series, { strokeStyle: '#00ff00', lineWidth: 2 }); chart.streamTo(canvas, 500); updateCode(canvas, chart); var controlContainer = document.getElementById('controls'), inputIdCounter = 0; var setVisible = function(element, isVisible) { element.style.display = isVisible ? '' : 'none'; }; var startControlSection = function(name) { var controlDiv = document.createElement('h3'); controlDiv.textContent = name; controlContainer.appendChild(controlDiv); }; var bindRange = function(options) { var inputId = 'input' + (inputIdCounter++), scale = options.scale || 1, defaultLink; var controlDiv = document.createElement('div'); controlDiv.className = 'control'; controlContainer.appendChild(controlDiv); var label = document.createElement('label'); label.className = 'slider'; label.htmlFor = inputId; label.textContent = options.name; controlDiv.appendChild(label); var slider = document.createElement('input'); slider.id = inputId; slider.type = 'range'; slider.min = options.min * scale; slider.max = options.max * scale; if (options.step) { slider.step = options.step * scale; } var defaultValue = options.target[options.propertyName]; slider.value = defaultValue * scale; slider.onchange = function() { options.target[options.propertyName] = parseInt(slider.value) / scale; setVisible(defaultLink, slider.value != defaultValue); updateCode(canvas, chart); }; controlDiv.appendChild(slider); defaultLink = document.createElement('a'); defaultLink.href = '#'; defaultLink.className = 'default'; defaultLink.textContent = 'default'; defaultLink.onclick = function() { slider.value = defaultValue * scale; slider.onchange(); return false; }; setVisible(defaultLink, slider.value != defaultValue); controlDiv.appendChild(defaultLink); }; var bindDropDown = function(options) { var inputId = 'input' + (inputIdCounter++), defaultLink; var controlDiv = document.createElement('div'); controlDiv.className = 'control'; controlContainer.appendChild(controlDiv); var label = document.createElement('label'); label.className = 'slider'; label.htmlFor = inputId; label.textContent = options.name; controlDiv.appendChild(label); var select = document.createElement('select'); select.id = inputId; var defaultValue = options.target[options.propertyName] || options.values[0]; select.value = defaultValue; for (var i = 0; i < options.values.length; i++) { var opt = document.createElement('option'); opt.textContent = options.values[i]; select.appendChild(opt); } select.onchange = function() { options.target[options.propertyName] = select.value; setVisible(defaultLink, select.value != defaultValue); updateCode(canvas, chart); }; controlDiv.appendChild(select); defaultLink = document.createElement('a'); defaultLink.href = '#'; defaultLink.className = 'default'; defaultLink.textContent = 'default'; defaultLink.onclick = function() { select.value = defaultValue; select.onchange(); return false; }; setVisible(defaultLink, select.value != defaultValue); controlDiv.appendChild(defaultLink); }; var bindCheckBox = function(options) { var inputId = 'input' + (inputIdCounter++), defaultLink; var controlDiv = document.createElement('div'); controlDiv.className = 'control'; controlContainer.appendChild(controlDiv); var checkbox = document.createElement('input'); checkbox.id = inputId; checkbox.type = 'checkbox'; var defaultValue = options.convertBack ? options.convertBack(options.target[options.propertyName]) : !!options.target[options.propertyName]; checkbox.checked = defaultValue; checkbox.onchange = function() { options.target[options.propertyName] = options.convert ? options.convert(checkbox.checked) : !!checkbox.checked; updateCode(canvas, chart); setVisible(defaultLink, checkbox.checked !== defaultValue); }; controlDiv.appendChild(checkbox); var label = document.createElement('label'); label.className = 'checkbox'; label.htmlFor = inputId; label.textContent = options.name; controlDiv.appendChild(label); defaultLink = document.createElement('a'); defaultLink.href = '#'; defaultLink.className = 'default'; defaultLink.textContent = 'default'; setVisible(defaultLink, checkbox.checked !== defaultValue); defaultLink.onclick = function() { checkbox.checked = defaultValue; checkbox.onchange(); return false; }; controlDiv.appendChild(defaultLink); }; var bindColor = function(options) { var inputId = 'input' + (inputIdCounter++), defaultValue = options.target[options.propertyName] || '#000000', colorString = defaultValue, defaultEnabled = options.optional ? options.enabled : true, enabled = defaultEnabled, enabledCheckbox, defaultLink, defaultOpacity = typeof(options.opacity) === 'undefined' ? 1 : options.opacity, opacity = defaultOpacity, opacitySlider, update = function() { if (!enabled) { if (options.emptyValue) options.target[options.propertyName] = options.emptyValue; else delete options.target[options.propertyName]; } else if (opacity === 0) { options.target[options.propertyName] = 'transparent'; } else if (opacity !== 1) { var result = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(colorString); options.target[options.propertyName] = 'rgba('+parseInt(result[1],16)+','+parseInt(result[2],16)+','+parseInt(result[3],16)+','+opacity.toFixed(2)+')'; } else { options.target[options.propertyName] = colorString; } setVisible(defaultLink, enabled != defaultEnabled || opacity !== defaultOpacity || (defaultValue && colorString !== defaultValue)); updateCode(canvas, chart); }; var controlDiv = document.createElement('div'); controlDiv.className = 'control'; var label = document.createElement('label'); label.className = 'color'; label.htmlFor = inputId; label.textContent = options.name; controlDiv.appendChild(label); controlContainer.appendChild(controlDiv); var colorInput = document.createElement('input'); colorInput.id = inputId; colorInput.type = 'color'; colorInput.value = defaultValue; colorString = colorInput.value; colorInput.onchange = function() { colorString = colorInput.value; update(); }; controlDiv.appendChild(colorInput); if (typeof(options.opacity) !== 'undefined') { opacitySlider = document.createElement('input'); opacitySlider.id = inputId + '-opacity'; opacitySlider.className = 'opacity'; opacitySlider.title = 'Opacity'; opacitySlider.type = 'range'; opacitySlider.min = 0; opacitySlider.max = 100; opacitySlider.value = defaultOpacity * 100; opacitySlider.onchange = function () { opacity = opacitySlider.value / 100; update(); }; controlDiv.appendChild(opacitySlider); } if (options.optional) { enabledCheckbox = document.createElement('input'); enabledCheckbox.id = inputId+'-enabled'; enabledCheckbox.type = 'checkbox'; enabledCheckbox.checked = defaultEnabled; enabledCheckbox.onchange = function() { enabled = enabledCheckbox.checked; update(); }; controlDiv.appendChild(enabledCheckbox); var enabledLabel = document.createElement('label'); enabledLabel.className = 'checkbox'; enabledLabel.htmlFor = enabledCheckbox.id; enabledLabel.textContent = 'Enabled'; controlDiv.appendChild(enabledLabel); } defaultLink = document.createElement('a'); defaultLink.href = '#'; defaultLink.className = 'default'; defaultLink.textContent = 'default'; defaultLink.onclick = function() { colorString = colorInput.value = defaultValue; opacity = defaultOpacity; if (opacitySlider) { opacitySlider.value = defaultOpacity * 100; } if (enabledCheckbox) { enabledCheckbox.checked = defaultEnabled; } enabled = defaultEnabled; update(); return false; }; controlDiv.appendChild(defaultLink); update(); }; // General startControlSection('General'); bindColor({target: chart.options.grid, name: 'Background color', propertyName: 'fillStyle', opacity: 1}); bindRange({target: chart.options, name: 'Scroll speed', propertyName: 'millisPerPixel', min: 1, max: 100}); bindRange({target: canvas, name: 'Chart height', propertyName: 'height', min: 20, max: 400}); bindRange({target: canvas, name: 'Chart width', propertyName: 'width', min: 20, max: 1000}); bindRange({target: chart, name: 'Delay', propertyName: 'delay', min: 0, max: 2000}); bindCheckBox({target: chart.options, name: 'Scroll Backwards', propertyName: 'scrollBackwards'}); bindCheckBox({target: chart.options, name: 'Show Tooltip', propertyName: 'tooltip'}); bindCheckBox({ target: chart.options, name: 'Limit FPS', propertyName: 'limitFPS', convert: function (checked) { return checked ? 15 : 0; }, convertBack: function (value) { return value !== 0; } }); // Series startControlSection('Series'); bindColor({target: chart.seriesSet[0].options, name: 'Series line color', propertyName: 'strokeStyle', optional: true, enabled: true, opacity: 1, emptyValue: 'none'}); bindColor({target: chart.seriesSet[0].options, name: 'Series fill color', propertyName: 'fillStyle', optional: true, enabled: false, opacity: 0.3}); bindRange({target: chart.seriesSet[0].options, name: 'Series line width', propertyName: 'lineWidth', min: 0.5, max: 5, scale: 10}); bindDropDown({ target: chart.seriesSet[0].options, name: 'Interpolation', propertyName: 'interpolation', values: ['bezier', 'linear', 'step'] }); // Grid lines startControlSection('Grid Lines'); bindColor({target: chart.options.grid, name: 'Grid line color', propertyName: 'strokeStyle', opacity: 1}); bindRange({target: chart.options.grid, name: 'Vertical sections', propertyName: 'verticalSections', min: 0, max: 20}); bindRange({target: chart.options.grid, name: 'Time line spacing', propertyName: 'millisPerLine', min: 1000, max: 10000, step: 1000}); bindCheckBox({target: chart.options.grid, name: 'Draw outer border', propertyName: 'borderVisible'}); bindCheckBox({ target: chart.options, name: 'Show y-value lines', propertyName: 'horizontalLines', convert: function (checked) { return checked ? sampleHorizontalLines : []; }, convertBack: function (value) { return value && !!value.length; } }); // Labels startControlSection('Labels'); bindColor({target: chart.options.labels, name: 'Label text color', propertyName: 'fillStyle', opacity: 1}); bindCheckBox({ target: chart.options.labels, name: 'Show max/min labels', propertyName: 'disabled', convert: function (checked) { return !checked; }, convertBack: function (value) { return !value; } }); bindCheckBox({ target: chart.options.labels, name: 'Show intermediate labels', propertyName: 'showIntermediateLabels', }); bindCheckBox({ target: chart.options.labels, name: 'Show intermediate labels on same axis', propertyName: 'intermediateLabelSameAxis', }); bindRange({target: chart.options.labels, name: 'Font size', propertyName: 'fontSize', min: 1, max: 20}); bindRange({target: chart.options.labels, name: 'Label precision', propertyName: 'precision', min: 0, max: 6}); bindCheckBox({ target: chart.options, name: 'Show timestamps', propertyName: 'timestampFormatter', convert: function (checked) { return checked ? SmoothieChart.timeFormatter : undefined; } }); // Tool tip startControlSection('Tool tip'); bindCheckBox({target: chart.options, name: 'Show tool tip on hover', propertyName: 'tooltip'}); bindRange({target: chart.options.tooltipLine, name: 'Tool tip line width', propertyName: 'lineWidth', min: 1, max: 5}); bindColor({target: chart.options.tooltipLine, name: 'Tool tip line color', propertyName: 'strokeStyle', opacity: 1}); // Y-scaling startControlSection('Y-scaling'); bindCheckBox({ target: chart.options, name: 'Fix maximum value', propertyName: 'maxValue', convert: function (checked) { return checked ? maxYValue : undefined; } }); bindCheckBox({ target: chart.options, name: 'Fix minimum value', propertyName: 'minValue', convert: function (checked) { return checked ? -maxYValue : undefined; } }); bindRange({target: chart.options, name: 'Max-value scale', propertyName: 'maxValueScale', min: 0.8, max: 1.5, scale: 100}); bindRange({target: chart.options, name: 'Min-value scale', propertyName: 'minValueScale', min: 0.8, max: 1.5, scale: 100}); bindRange({target: chart.options, name: 'Scale adjust speed', propertyName: 'scaleSmoothing', min: .01, max: 1, scale: 1000}); bindCheckBox({ target: chart.options, name: 'Custom y-value range function', propertyName: 'yRangeFunction', convert: function (checked) { return checked ? customYRangeFunction : undefined; } }); } </script> <style> body { padding: 0 26px; background-color: #888; color: #000; font-size: 13px; font-family: Helvetica, arial, freesans, clean, sans-serif; } h1 { font-family: Pacifico, sans-serif; font-size: 32px; text-shadow: 2px 2px 1px #444; color: #EEE; } h3 { font-family: Pacifico, sans-serif; width: 700px; border-top: 1px dotted #aaa; padding-top: 4px; margin-bottom: 4px; color: #ccc; text-shadow: 1px 1px 2px #000; } input[type='checkbox'] { margin-left: 0; } #controls a.default { margin-left: 1em; font-size: 10px; color: #444; } #controls { margin-top: 26px; } #controls div.control { min-height: 26px; } #controls div.control input.opacity { margin: 0 12px; } #controls div.control input[type='checkbox'] { margin-top: 10px; } #controls div.control label.slider, #controls div.control label.color { display: inline-block; width: 150px; } #code > div { margin-top: 14px; } #code textarea { box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.45); padding: 8px; width: 700px; border: 1px solid black; box-sizing: border-box; } div.smoothie-chart-tooltip { background: #444; padding: 1em; margin-top: 20px; font-family: consolas; color: white; font-size: 10px; pointer-events: none; } </style> </head> <body onload="onLoad()"> <h1>Smoothie Charts Builder</h1> <canvas id="chart" width="500" height="100"></canvas> <div id="controls"></div> <h3>Code</h3> <div id="code"> <div> <label for="html-code">HTML:</label><br> <textarea readonly id="html-code" style="height:34px;"></textarea> </div> <div> <label for="javascript-code">JavaScript:</label><br> <textarea readonly id="javascript-code" style="min-height:140px;"></textarea> </div> <div id="css-code"> <label for="css-code">CSS:</label><br> <textarea readonly id="css-code" style="min-height:160px;">div.smoothie-chart-tooltip { background: #444; padding: 1em; margin-top: 20px; font-family: consolas; color: white; font-size: 10px; pointer-events: none; } </textarea> </div> </div> </body> </html>