-
22 |
- 23 | 24 | 25 | 26 |
├── doc ├── vmstat-web.png ├── vmstat-web1.png └── swoole-vmstat.conf ├── web ├── img │ └── forkme_right_orange_ff7600.png ├── stats.css ├── index.html ├── stats.js └── js │ ├── reconnecting-websocket.js │ ├── smoothie.js │ ├── chroma.min.js │ ├── sugar-1.4.1.min.js │ └── jquery-2.0.3.min.js ├── log └── swoole.log ├── README.md ├── server └── server.php └── LICENSE /doc/vmstat-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-vmstat/HEAD/doc/vmstat-web.png -------------------------------------------------------------------------------- /doc/vmstat-web1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-vmstat/HEAD/doc/vmstat-web1.png -------------------------------------------------------------------------------- /web/img/forkme_right_orange_ff7600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxmc/swoole-vmstat/HEAD/web/img/forkme_right_orange_ff7600.png -------------------------------------------------------------------------------- /log/swoole.log: -------------------------------------------------------------------------------- 1 | [2019-11-22 13:55:31 *20835.0] WARNING swWorker_reactor_is_empty (ERRNO 9012): worker exit timeout, forced to terminate 2 | -------------------------------------------------------------------------------- /doc/swoole-vmstat.conf: -------------------------------------------------------------------------------- 1 | #设定虚拟主机配置 2 | server { 3 | #侦听80端口 4 | listen 80; 5 | server_name vmstat.iizhu.com; 6 | 7 | #定义服务器的默认网站根目录位置 8 | root /var/www/code/swoole-vmstat/web/; 9 | 10 | location / { 11 | #定义首页索引文件的名称 12 | index index.php index.html index.htm; 13 | 14 | } 15 | 16 | # 定义错误提示页面 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | } 20 | 21 | #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置. 22 | location ~ .php$ { 23 | fastcgi_pass 127.0.0.1:9000; 24 | fastcgi_index index.php; 25 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 26 | include fastcgi_params; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /web/stats.css: -------------------------------------------------------------------------------- 1 | .template { 2 | display: none !important; 3 | } 4 | * { 5 | cursor: default; 6 | } 7 | body { 8 | background-color: #333; 9 | color: #eee; 10 | font-family: "helvetica neue", helvetica, arial, sans-serif; 11 | } 12 | h2 { 13 | margin: 40px 0 0 0; 14 | font-weight: 300; 15 | } 16 | main { 17 | width: 600px; 18 | margin: auto; 19 | } 20 | section { 21 | clear: left; 22 | } 23 | .stats { 24 | margin: 0; 25 | } 26 | .stat { 27 | list-style-type: none; 28 | float: left; 29 | margin: 0; 30 | width: 130px; 31 | font-size: 12px; 32 | } 33 | .stat-name { 34 | display: inline-block; 35 | text-align: right; 36 | width: 50px; 37 | margin-right: 5px; 38 | } 39 | .stat-value { 40 | font-weight: bold; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 |
15 |
16 |
17 | TimeSeries with optional data options.
95 | *
96 | * Options are of the form (defaults shown):
97 | *
98 | *
99 | * {
100 | * resetBounds: true, // enables/disables automatic scaling of the y-axis
101 | * resetBoundsInterval: 3000 // the period between scaling calculations, in millis
102 | * }
103 | *
104 | *
105 | * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries.
106 | *
107 | * @constructor
108 | */
109 | function TimeSeries(options) {
110 | this.options = Util.extend({}, TimeSeries.defaultOptions, options);
111 | this.data = [];
112 | this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries.
113 | this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries.
114 | }
115 |
116 | TimeSeries.defaultOptions = {
117 | resetBoundsInterval: 3000,
118 | resetBounds: true
119 | };
120 |
121 | /**
122 | * Recalculate the min/max values for this TimeSeries object.
123 | *
124 | * This causes the graph to scale itself in the y-axis.
125 | */
126 | TimeSeries.prototype.resetBounds = function() {
127 | if (this.data.length) {
128 | // Walk through all data points, finding the min/max value
129 | this.maxValue = this.data[0][1];
130 | this.minValue = this.data[0][1];
131 | for (var i = 1; i < this.data.length; i++) {
132 | var value = this.data[i][1];
133 | if (value > this.maxValue) {
134 | this.maxValue = value;
135 | }
136 | if (value < this.minValue) {
137 | this.minValue = value;
138 | }
139 | }
140 | } else {
141 | // No data exists, so set min/max to NaN
142 | this.maxValue = Number.NaN;
143 | this.minValue = Number.NaN;
144 | }
145 | };
146 |
147 | /**
148 | * Adds a new data point to the TimeSeries, preserving chronological order.
149 | *
150 | * @param timestamp the position, in time, of this data point
151 | * @param value the value of this data point
152 | * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls
153 | * whether it is replaced, or the values summed (defaults to false.)
154 | */
155 | TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) {
156 | // Rewind until we hit an older timestamp
157 | var i = this.data.length - 1;
158 | while (i > 0 && this.data[i][0] > timestamp) {
159 | i--;
160 | }
161 |
162 | if (this.data.length > 0 && this.data[i][0] === timestamp) {
163 | // Update existing values in the array
164 | if (sumRepeatedTimeStampValues) {
165 | // Sum this value into the existing 'bucket'
166 | this.data[i][1] += value;
167 | value = this.data[i][1];
168 | } else {
169 | // Replace the previous value
170 | this.data[i][1] = value;
171 | }
172 | } else if (i < this.data.length - 1) {
173 | // Splice into the correct position to keep timestamps in order
174 | this.data.splice(i + 1, 0, [timestamp, value]);
175 | } else {
176 | // Add to the end of the array
177 | this.data.push([timestamp, value]);
178 | }
179 |
180 | this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value);
181 | this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value);
182 | };
183 |
184 | TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) {
185 | // We must always keep one expired data point as we need this to draw the
186 | // line that comes into the chart from the left, but any points prior to that can be removed.
187 | var removeCount = 0;
188 | while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) {
189 | removeCount++;
190 | }
191 | if (removeCount !== 0) {
192 | this.data.splice(0, removeCount);
193 | }
194 | };
195 |
196 | /**
197 | * Initialises a new SmoothieChart.
198 | *
199 | * Options are optional, and should be of the form below. Just specify the values you
200 | * need and the rest will be given sensible defaults as shown:
201 | *
202 | *
203 | * {
204 | * minValue: undefined, // specify to clamp the lower y-axis to a given value
205 | * maxValue: undefined, // specify to clamp the upper y-axis to a given value
206 | * maxValueScale: 1, // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
207 | * yRangeFunction: undefined, // function({min: , max: }) { return {min: , max: }; }
208 | * scaleSmoothing: 0.125, // controls the rate at which y-value zoom animation occurs
209 | * millisPerPixel: 20, // sets the speed at which the chart pans by
210 | * maxDataSetLength: 2,
211 | * interpolation: 'bezier' // or 'linear'
212 | * timestampFormatter: null, // Optional function to format time stamps for bottom of chart. You may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
213 | * horizontalLines: [], // [ { value: 0, color: '#ffffff', lineWidth: 1 } ],
214 | * grid:
215 | * {
216 | * fillStyle: '#000000', // the background colour of the chart
217 | * lineWidth: 1, // the pixel width of grid lines
218 | * strokeStyle: '#777777', // colour of grid lines
219 | * millisPerLine: 1000, // distance between vertical grid lines
220 | * sharpLines: false, // controls whether grid lines are 1px sharp, or softened
221 | * verticalSections: 2, // number of vertical sections marked out by horizontal grid lines
222 | * borderVisible: true // whether the grid lines trace the border of the chart or not
223 | * },
224 | * labels
225 | * {
226 | * disabled: false, // enables/disables labels showing the min/max values
227 | * fillStyle: '#ffffff', // colour for text of labels,
228 | * fontSize: 15,
229 | * fontFamily: 'sans-serif',
230 | * precision: 2
231 | * },
232 | * }
233 | *
234 | *
235 | * @constructor
236 | */
237 | function SmoothieChart(options) {
238 | this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options);
239 | this.seriesSet = [];
240 | this.currentValueRange = 1;
241 | this.currentVisMinValue = 0;
242 | this.lastRenderTimeMillis = 0;
243 | }
244 |
245 | SmoothieChart.defaultChartOptions = {
246 | millisPerPixel: 20,
247 | maxValueScale: 1,
248 | interpolation: 'bezier',
249 | scaleSmoothing: 0.125,
250 | maxDataSetLength: 2,
251 | grid: {
252 | fillStyle: '#000000',
253 | strokeStyle: '#777777',
254 | lineWidth: 1,
255 | sharpLines: false,
256 | millisPerLine: 1000,
257 | verticalSections: 2,
258 | borderVisible: true
259 | },
260 | labels: {
261 | fillStyle: '#ffffff',
262 | disabled: false,
263 | fontSize: 10,
264 | fontFamily: 'monospace',
265 | precision: 2
266 | },
267 | horizontalLines: []
268 | };
269 |
270 | // Based on http://inspirit.github.com/jsfeat/js/compatibility.js
271 | SmoothieChart.AnimateCompatibility = (function() {
272 | var requestAnimationFrame = function(callback, element) {
273 | var requestAnimationFrame =
274 | window.requestAnimationFrame ||
275 | window.webkitRequestAnimationFrame ||
276 | window.mozRequestAnimationFrame ||
277 | window.oRequestAnimationFrame ||
278 | window.msRequestAnimationFrame ||
279 | function(callback) {
280 | return window.setTimeout(function() {
281 | callback(new Date().getTime());
282 | }, 16);
283 | };
284 | return requestAnimationFrame.call(window, callback, element);
285 | },
286 | cancelAnimationFrame = function(id) {
287 | var cancelAnimationFrame =
288 | window.cancelAnimationFrame ||
289 | function(id) {
290 | clearTimeout(id);
291 | };
292 | return cancelAnimationFrame.call(window, id);
293 | };
294 |
295 | return {
296 | requestAnimationFrame: requestAnimationFrame,
297 | cancelAnimationFrame: cancelAnimationFrame
298 | };
299 | })();
300 |
301 | SmoothieChart.defaultSeriesPresentationOptions = {
302 | lineWidth: 1,
303 | strokeStyle: '#ffffff'
304 | };
305 |
306 | /**
307 | * Adds a TimeSeries to this chart, with optional presentation options.
308 | *
309 | * Presentation options should be of the form (defaults shown):
310 | *
311 | *
312 | * {
313 | * lineWidth: 1,
314 | * strokeStyle: '#ffffff',
315 | * fillStyle: undefined
316 | * }
317 | *
318 | */
319 | SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
320 | this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)});
321 | if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) {
322 | timeSeries.resetBoundsTimerId = setInterval(
323 | function() {
324 | timeSeries.resetBounds();
325 | },
326 | timeSeries.options.resetBoundsInterval
327 | );
328 | }
329 | };
330 |
331 | /**
332 | * Removes the specified TimeSeries from the chart.
333 | */
334 | SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
335 | // Find the correct timeseries to remove, and remove it
336 | var numSeries = this.seriesSet.length;
337 | for (var i = 0; i < numSeries; i++) {
338 | if (this.seriesSet[i].timeSeries === timeSeries) {
339 | this.seriesSet.splice(i, 1);
340 | break;
341 | }
342 | }
343 | // If a timer was operating for that timeseries, remove it
344 | if (timeSeries.resetBoundsTimerId) {
345 | // Stop resetting the bounds, if we were
346 | clearInterval(timeSeries.resetBoundsTimerId);
347 | }
348 | };
349 |
350 | /**
351 | * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay.
352 | *
353 | * @param canvas the target canvas element
354 | * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series
355 | * from appearing on screen, with new values flashing into view, at the expense of some latency.
356 | */
357 | SmoothieChart.prototype.streamTo = function(canvas, delayMillis) {
358 | this.canvas = canvas;
359 | this.delay = delayMillis;
360 | this.start();
361 | };
362 |
363 | /**
364 | * Starts the animation of this chart.
365 | */
366 | SmoothieChart.prototype.start = function() {
367 | if (this.frame) {
368 | // We're already running, so just return
369 | return;
370 | }
371 |
372 | // Renders a frame, and queues the next frame for later rendering
373 | var animate = function() {
374 | this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
375 | this.render();
376 | animate();
377 | }.bind(this));
378 | }.bind(this);
379 |
380 | animate();
381 | };
382 |
383 | /**
384 | * Stops the animation of this chart.
385 | */
386 | SmoothieChart.prototype.stop = function() {
387 | if (this.frame) {
388 | SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
389 | delete this.frame;
390 | }
391 | };
392 |
393 | SmoothieChart.prototype.updateValueRange = function() {
394 | // Calculate the current scale of the chart, from all time series.
395 | var chartOptions = this.options,
396 | chartMaxValue = Number.NaN,
397 | chartMinValue = Number.NaN;
398 |
399 | for (var d = 0; d < this.seriesSet.length; d++) {
400 | // TODO(ndunn): We could calculate / track these values as they stream in.
401 | var timeSeries = this.seriesSet[d].timeSeries;
402 | if (!isNaN(timeSeries.maxValue)) {
403 | chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue;
404 | }
405 |
406 | if (!isNaN(timeSeries.minValue)) {
407 | chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue;
408 | }
409 | }
410 |
411 | // Scale the chartMaxValue to add padding at the top if required
412 | if (chartOptions.maxValue != null) {
413 | chartMaxValue = chartOptions.maxValue;
414 | } else {
415 | chartMaxValue *= chartOptions.maxValueScale;
416 | }
417 |
418 | // Set the minimum if we've specified one
419 | if (chartOptions.minValue != null) {
420 | chartMinValue = chartOptions.minValue;
421 | }
422 |
423 | // If a custom range function is set, call it
424 | if (this.options.yRangeFunction) {
425 | var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue});
426 | chartMinValue = range.min;
427 | chartMaxValue = range.max;
428 | }
429 |
430 | if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) {
431 | var targetValueRange = chartMaxValue - chartMinValue;
432 | var valueRangeDiff = (targetValueRange - this.currentValueRange);
433 | var minValueDiff = (chartMinValue - this.currentVisMinValue);
434 | this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1;
435 | this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff;
436 | this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff;
437 | }
438 |
439 | this.valueRange = { min: chartMinValue, max: chartMaxValue };
440 | };
441 |
442 | SmoothieChart.prototype.render = function(canvas, time) {
443 | var nowMillis = new Date().getTime();
444 |
445 | if (!this.isAnimatingScale) {
446 | // We're not animating. We can use the last render time and the scroll speed to work out whether
447 | // we actually need to paint anything yet. If not, we can return immediately.
448 |
449 | // Render at least every 1/6th of a second. The canvas may be resized, which there is
450 | // no reliable way to detect.
451 | var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
452 |
453 | if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
454 | return;
455 | }
456 | }
457 | this.lastRenderTimeMillis = nowMillis;
458 |
459 | canvas = canvas || this.canvas;
460 | time = time || nowMillis - (this.delay || 0);
461 |
462 | // Round time down to pixel granularity, so motion appears smoother.
463 | time -= time % this.options.millisPerPixel;
464 |
465 | var context = canvas.getContext('2d'),
466 | chartOptions = this.options,
467 | dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
468 | // Calculate the threshold time for the oldest data points.
469 | oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel),
470 | valueToYPixel = function(value) {
471 | var offset = value - this.currentVisMinValue;
472 | return this.currentValueRange === 0
473 | ? dimensions.height
474 | : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height));
475 | }.bind(this),
476 | timeToXPixel = function(t) {
477 | return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel));
478 | };
479 |
480 | this.updateValueRange();
481 |
482 | context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily;
483 |
484 | // Save the state of the canvas context, any transformations applied in this method
485 | // will get removed from the stack at the end of this method when .restore() is called.
486 | context.save();
487 |
488 | // Move the origin.
489 | context.translate(dimensions.left, dimensions.top);
490 |
491 | // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
492 | // This prevents the occasional pixels from curves near the edges overrunning and creating
493 | // screen cheese (that phrase should need no explanation).
494 | context.beginPath();
495 | context.rect(0, 0, dimensions.width, dimensions.height);
496 | context.clip();
497 |
498 | // Clear the working area.
499 | context.save();
500 | context.fillStyle = chartOptions.grid.fillStyle;
501 | context.clearRect(0, 0, dimensions.width, dimensions.height);
502 | context.fillRect(0, 0, dimensions.width, dimensions.height);
503 | context.restore();
504 |
505 | // Grid lines...
506 | context.save();
507 | context.lineWidth = chartOptions.grid.lineWidth;
508 | context.strokeStyle = chartOptions.grid.strokeStyle;
509 | // Vertical (time) dividers.
510 | if (chartOptions.grid.millisPerLine > 0) {
511 | var textUntilX = dimensions.width - context.measureText(minValueString).width + 4;
512 | for (var t = time - (time % chartOptions.grid.millisPerLine);
513 | t >= oldestValidTime;
514 | t -= chartOptions.grid.millisPerLine) {
515 | var gx = timeToXPixel(t);
516 | if (chartOptions.grid.sharpLines) {
517 | gx -= 0.5;
518 | }
519 | context.beginPath();
520 | context.moveTo(gx, 0);
521 | context.lineTo(gx, dimensions.height);
522 | context.stroke();
523 | context.closePath();
524 |
525 | // Display timestamp at bottom of this line if requested, and it won't overlap
526 | if (chartOptions.timestampFormatter && gx < textUntilX) {
527 | // Formats the timestamp based on user specified formatting function
528 | // SmoothieChart.timeFormatter function above is one such formatting option
529 | var tx = new Date(t),
530 | ts = chartOptions.timestampFormatter(tx),
531 | tsWidth = context.measureText(ts).width;
532 | textUntilX = gx - tsWidth - 2;
533 | context.fillStyle = chartOptions.labels.fillStyle;
534 | context.fillText(ts, gx - tsWidth, dimensions.height - 2);
535 | }
536 | }
537 | }
538 |
539 | // Horizontal (value) dividers.
540 | for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
541 | var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections);
542 | if (chartOptions.grid.sharpLines) {
543 | gy -= 0.5;
544 | }
545 | context.beginPath();
546 | context.moveTo(0, gy);
547 | context.lineTo(dimensions.width, gy);
548 | context.stroke();
549 | context.closePath();
550 | }
551 | // Bounding rectangle.
552 | if (chartOptions.grid.borderVisible) {
553 | context.beginPath();
554 | context.strokeRect(0, 0, dimensions.width, dimensions.height);
555 | context.closePath();
556 | }
557 | context.restore();
558 |
559 | // Draw any horizontal lines...
560 | if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) {
561 | for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) {
562 | var line = chartOptions.horizontalLines[hl],
563 | hly = Math.round(valueToYPixel(line.value)) - 0.5;
564 | context.strokeStyle = line.color || '#ffffff';
565 | context.lineWidth = line.lineWidth || 1;
566 | context.beginPath();
567 | context.moveTo(0, hly);
568 | context.lineTo(dimensions.width, hly);
569 | context.stroke();
570 | context.closePath();
571 | }
572 | }
573 |
574 | // For each data set...
575 | for (var d = 0; d < this.seriesSet.length; d++) {
576 | context.save();
577 | var timeSeries = this.seriesSet[d].timeSeries,
578 | dataSet = timeSeries.data,
579 | seriesOptions = this.seriesSet[d].options;
580 |
581 | // Delete old data that's moved off the left of the chart.
582 | timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength);
583 |
584 | // Set style for this dataSet.
585 | context.lineWidth = seriesOptions.lineWidth;
586 | context.strokeStyle = seriesOptions.strokeStyle;
587 | // Draw the line...
588 | context.beginPath();
589 | // Retain lastX, lastY for calculating the control points of bezier curves.
590 | var firstX = 0, lastX = 0, lastY = 0;
591 | for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) {
592 | var x = timeToXPixel(dataSet[i][0]),
593 | y = valueToYPixel(dataSet[i][1]);
594 |
595 | if (i === 0) {
596 | firstX = x;
597 | context.moveTo(x, y);
598 | } else {
599 | switch (chartOptions.interpolation) {
600 | case "linear":
601 | case "line": {
602 | context.lineTo(x,y);
603 | break;
604 | }
605 | case "bezier":
606 | default: {
607 | // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
608 | //
609 | // Assuming A was the last point in the line plotted and B is the new point,
610 | // we draw a curve with control points P and Q as below.
611 | //
612 | // A---P
613 | // |
614 | // |
615 | // |
616 | // Q---B
617 | //
618 | // Importantly, A and P are at the same y coordinate, as are B and Q. This is
619 | // so adjacent curves appear to flow as one.
620 | //
621 | context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
622 | Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
623 | Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
624 | x, y); // endPoint (B)
625 | break;
626 | }
627 | }
628 | }
629 |
630 | lastX = x; lastY = y;
631 | }
632 |
633 | if (dataSet.length > 1) {
634 | if (seriesOptions.fillStyle) {
635 | // Close up the fill region.
636 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
637 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
638 | context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
639 | context.fillStyle = seriesOptions.fillStyle;
640 | context.fill();
641 | }
642 |
643 | if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') {
644 | context.stroke();
645 | }
646 | context.closePath();
647 | }
648 | context.restore();
649 | }
650 |
651 | // Draw the axis values on the chart.
652 | if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
653 | var maxValueString = parseFloat(this.valueRange.max).toFixed(chartOptions.labels.precision),
654 | minValueString = parseFloat(this.valueRange.min).toFixed(chartOptions.labels.precision);
655 | context.fillStyle = chartOptions.labels.fillStyle;
656 | context.fillText(maxValueString, dimensions.width - context.measureText(maxValueString).width - 2, chartOptions.labels.fontSize);
657 | context.fillText(minValueString, dimensions.width - context.measureText(minValueString).width - 2, dimensions.height - 2);
658 | }
659 |
660 | context.restore(); // See .save() above.
661 | };
662 |
663 | // Sample timestamp formatting function
664 | SmoothieChart.timeFormatter = function(date) {
665 | function pad2(number) { return (number < 10 ? '0' : '') + number }
666 | return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds());
667 | };
668 |
669 | exports.TimeSeries = TimeSeries;
670 | exports.SmoothieChart = SmoothieChart;
671 |
672 | })(typeof exports === 'undefined' ? this : exports);
673 |
674 |
--------------------------------------------------------------------------------
/web/js/chroma.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | *
4 | * chroma.js - JavaScript library for color conversions
5 | *
6 | * Copyright (c) 2011-2013, Gregor Aisch
7 | * All rights reserved.
8 | *
9 | * Redistribution and use in source and binary forms, with or without
10 | * modification, are permitted provided that the following conditions are met:
11 | *
12 | * 1. Redistributions of source code must retain the above copyright notice, this
13 | * list of conditions and the following disclaimer.
14 | *
15 | * 2. Redistributions in binary form must reproduce the above copyright notice,
16 | * this list of conditions and the following disclaimer in the documentation
17 | * and/or other materials provided with the distribution.
18 | *
19 | * 3. The name Gregor Aisch may not be used to endorse or promote products
20 | * derived from this software without specific prior written permission.
21 | *
22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25 | * DISCLAIMED. IN NO EVENT SHALL GREGOR AISCH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
26 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
29 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
31 | * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 | */
33 | !function(){var Color,K,PITHIRD,TWOPI,X,Y,Z,bezier,brewer,chroma,clip_rgb,colors,cos,css2rgb,hex2rgb,hsi2rgb,hsl2rgb,hsv2rgb,lab2lch,lab2rgb,lab_xyz,lch2lab,lch2rgb,limit,luminance,luminance_x,rgb2hex,rgb2hsi,rgb2hsl,rgb2hsv,rgb2lab,rgb2lch,rgb_xyz,root,type,unpack,xyz_lab,xyz_rgb,_ref;chroma=function(x,y,z,m){return new Color(x,y,z,m)};if(typeof module!=="undefined"&&module!==null&&module.exports!=null){module.exports=chroma}if(typeof define==="function"&&define.amd){define([],function(){return chroma})}else{root=typeof exports!=="undefined"&&exports!==null?exports:this;root.chroma=chroma}chroma.color=function(x,y,z,m){return new Color(x,y,z,m)};chroma.hsl=function(h,s,l,a){return new Color(h,s,l,a,"hsl")};chroma.hsv=function(h,s,v,a){return new Color(h,s,v,a,"hsv")};chroma.rgb=function(r,g,b,a){return new Color(r,g,b,a,"rgb")};chroma.hex=function(x){return new Color(x)};chroma.css=function(x){return new Color(x)};chroma.lab=function(l,a,b){return new Color(l,a,b,"lab")};chroma.lch=function(l,c,h){return new Color(l,c,h,"lch")};chroma.hsi=function(h,s,i){return new Color(h,s,i,"hsi")};chroma.gl=function(r,g,b,a){return new Color(r*255,g*255,b*255,a,"gl")};chroma.interpolate=function(a,b,f,m){if(a==null||b==null){return"#000"}if(type(a)==="string"){a=new Color(a)}if(type(b)==="string"){b=new Color(b)}return a.interpolate(f,b,m)};chroma.mix=chroma.interpolate;chroma.contrast=function(a,b){var l1,l2;if(type(a)==="string"){a=new Color(a)}if(type(b)==="string"){b=new Color(b)}l1=a.luminance();l2=b.luminance();if(l1>l2){return(l1+.05)/(l2+.05)}else{return(l2+.05)/(l1+.05)}};chroma.luminance=function(color){return chroma(color).luminance()};chroma._Color=Color;Color=function(){function Color(){var a,arg,args,m,me,me_rgb,x,y,z,_i,_len,_ref,_ref1,_ref2,_ref3;me=this;args=[];for(_i=0,_len=arguments.length;_i<_len;_i++){arg=arguments[_i];if(arg!=null){args.push(arg)}}if(args.length===0){_ref=[255,0,255,1,"rgb"],x=_ref[0],y=_ref[1],z=_ref[2],a=_ref[3],m=_ref[4]}else if(type(args[0])==="array"){if(args[0].length===3){_ref1=args[0],x=_ref1[0],y=_ref1[1],z=_ref1[2];a=1}else if(args[0].length===4){_ref2=args[0],x=_ref2[0],y=_ref2[1],z=_ref2[2],a=_ref2[3]}else{throw"unknown input argument"}m=args[1]}else if(type(args[0])==="string"){x=args[0];m="hex"}else if(type(args[0])==="object"){_ref3=args[0]._rgb,x=_ref3[0],y=_ref3[1],z=_ref3[2],a=_ref3[3];m="rgb"}else if(args.length>=3){x=args[0];y=args[1];z=args[2]}if(args.length===3){m="rgb";a=1}else if(args.length===4){if(type(args[3])==="string"){m=args[3];a=1}else if(type(args[3])==="number"){m="rgb";a=args[3]}}else if(args.length===5){a=args[3];m=args[4]}if(a==null){a=1}if(m==="rgb"){me._rgb=[x,y,z,a]}else if(m==="gl"){me._rgb=[x*255,y*255,z*255,a]}else if(m==="hsl"){me._rgb=hsl2rgb(x,y,z);me._rgb[3]=a}else if(m==="hsv"){me._rgb=hsv2rgb(x,y,z);me._rgb[3]=a}else if(m==="hex"){me._rgb=hex2rgb(x)}else if(m==="lab"){me._rgb=lab2rgb(x,y,z);me._rgb[3]=a}else if(m==="lch"){me._rgb=lch2rgb(x,y,z);me._rgb[3]=a}else if(m==="hsi"){me._rgb=hsi2rgb(x,y,z);me._rgb[3]=a}me_rgb=clip_rgb(me._rgb)}Color.prototype.rgb=function(){return this._rgb.slice(0,3)};Color.prototype.rgba=function(){return this._rgb};Color.prototype.hex=function(){return rgb2hex(this._rgb)};Color.prototype.toString=function(){return this.name()};Color.prototype.hsl=function(){return rgb2hsl(this._rgb)};Color.prototype.hsv=function(){return rgb2hsv(this._rgb)};Color.prototype.lab=function(){return rgb2lab(this._rgb)};Color.prototype.lch=function(){return rgb2lch(this._rgb)};Color.prototype.hsi=function(){return rgb2hsi(this._rgb)};Color.prototype.gl=function(){return[this._rgb[0]/255,this._rgb[1]/255,this._rgb[2]/255,this._rgb[3]]};Color.prototype.luminance=function(){return luminance(this._rgb)};Color.prototype.name=function(){var h,k;h=this.hex();for(k in chroma.colors){if(h===chroma.colors[k]){return k}}return h};Color.prototype.alpha=function(alpha){if(arguments.length){this._rgb[3]=alpha;return this}return this._rgb[3]};Color.prototype.css=function(mode){var hsl,me,rgb,rnd;if(mode==null){mode="rgb"}me=this;rgb=me._rgb;if(mode.length===3&&rgb[3]<1){mode+="a"}if(mode==="rgb"){return mode+"("+rgb.slice(0,3).join(",")+")"}else if(mode==="rgba"){return mode+"("+rgb.join(",")+")"}else if(mode==="hsl"||mode==="hsla"){hsl=me.hsl();rnd=function(a){return Math.round(a*100)/100};hsl[0]=rnd(hsl[0]);hsl[1]=rnd(hsl[1]*100)+"%";hsl[2]=rnd(hsl[2]*100)+"%";if(mode.length===4){hsl[3]=rgb[3]}return mode+"("+hsl.join(",")+")"}};Color.prototype.interpolate=function(f,col,m){var dh,hue,hue0,hue1,lbv,lbv0,lbv1,me,res,sat,sat0,sat1,xyz0,xyz1;me=this;if(m==null){m="rgb"}if(type(col)==="string"){col=new Color(col)}if(m==="hsl"||m==="hsv"||m==="lch"||m==="hsi"){if(m==="hsl"){xyz0=me.hsl();xyz1=col.hsl()}else if(m==="hsv"){xyz0=me.hsv();xyz1=col.hsv()}else if(m==="hsi"){xyz0=me.hsi();xyz1=col.hsi()}else if(m==="lch"){xyz0=me.lch();xyz1=col.lch()}if(m.substr(0,1)==="h"){hue0=xyz0[0],sat0=xyz0[1],lbv0=xyz0[2];hue1=xyz1[0],sat1=xyz1[1],lbv1=xyz1[2]}else{lbv0=xyz0[0],sat0=xyz0[1],hue0=xyz0[2];lbv1=xyz1[0],sat1=xyz1[1],hue1=xyz1[2]}if(!isNaN(hue0)&&!isNaN(hue1)){if(hue1>hue0&&hue1-hue0>180){dh=hue1-(hue0+360)}else if(hue1