Phi: property browser can do charts
This commit is contained in:
parent
15c6160c05
commit
59e0fa180a
5 changed files with 678 additions and 74 deletions
432
webgui/3rdparty/flot/jquery.flot.time.js
vendored
Normal file
432
webgui/3rdparty/flot/jquery.flot.time.js
vendored
Normal file
|
@ -0,0 +1,432 @@
|
|||
/* Pretty handling of time axes.
|
||||
|
||||
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||
Licensed under the MIT license.
|
||||
|
||||
Set axis.mode to "time" to enable. See the section "Time series data" in
|
||||
API.txt for details.
|
||||
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
|
||||
var options = {
|
||||
xaxis: {
|
||||
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
||||
timeformat: null, // format string to use
|
||||
twelveHourClock: false, // 12 or 24 time in time mode
|
||||
monthNames: null // list of names of months
|
||||
}
|
||||
};
|
||||
|
||||
// round to nearby lower multiple of base
|
||||
|
||||
function floorInBase(n, base) {
|
||||
return base * Math.floor(n / base);
|
||||
}
|
||||
|
||||
// Returns a string with the date d formatted according to fmt.
|
||||
// A subset of the Open Group's strftime format is supported.
|
||||
|
||||
function formatDate(d, fmt, monthNames, dayNames) {
|
||||
|
||||
if (typeof d.strftime == "function") {
|
||||
return d.strftime(fmt);
|
||||
}
|
||||
|
||||
var leftPad = function(n, pad) {
|
||||
n = "" + n;
|
||||
pad = "" + (pad == null ? "0" : pad);
|
||||
return n.length == 1 ? pad + n : n;
|
||||
};
|
||||
|
||||
var r = [];
|
||||
var escape = false;
|
||||
var hours = d.getHours();
|
||||
var isAM = hours < 12;
|
||||
|
||||
if (monthNames == null) {
|
||||
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
}
|
||||
|
||||
if (dayNames == null) {
|
||||
dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
}
|
||||
|
||||
var hours12;
|
||||
|
||||
if (hours > 12) {
|
||||
hours12 = hours - 12;
|
||||
} else if (hours == 0) {
|
||||
hours12 = 12;
|
||||
} else {
|
||||
hours12 = hours;
|
||||
}
|
||||
|
||||
for (var i = 0; i < fmt.length; ++i) {
|
||||
|
||||
var c = fmt.charAt(i);
|
||||
|
||||
if (escape) {
|
||||
switch (c) {
|
||||
case 'a': c = "" + dayNames[d.getDay()]; break;
|
||||
case 'b': c = "" + monthNames[d.getMonth()]; break;
|
||||
case 'd': c = leftPad(d.getDate()); break;
|
||||
case 'e': c = leftPad(d.getDate(), " "); break;
|
||||
case 'h': // For back-compat with 0.7; remove in 1.0
|
||||
case 'H': c = leftPad(hours); break;
|
||||
case 'I': c = leftPad(hours12); break;
|
||||
case 'l': c = leftPad(hours12, " "); break;
|
||||
case 'm': c = leftPad(d.getMonth() + 1); break;
|
||||
case 'M': c = leftPad(d.getMinutes()); break;
|
||||
// quarters not in Open Group's strftime specification
|
||||
case 'q':
|
||||
c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
|
||||
case 'S': c = leftPad(d.getSeconds()); break;
|
||||
case 'y': c = leftPad(d.getFullYear() % 100); break;
|
||||
case 'Y': c = "" + d.getFullYear(); break;
|
||||
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
|
||||
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
|
||||
case 'w': c = "" + d.getDay(); break;
|
||||
}
|
||||
r.push(c);
|
||||
escape = false;
|
||||
} else {
|
||||
if (c == "%") {
|
||||
escape = true;
|
||||
} else {
|
||||
r.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r.join("");
|
||||
}
|
||||
|
||||
// To have a consistent view of time-based data independent of which time
|
||||
// zone the client happens to be in we need a date-like object independent
|
||||
// of time zones. This is done through a wrapper that only calls the UTC
|
||||
// versions of the accessor methods.
|
||||
|
||||
function makeUtcWrapper(d) {
|
||||
|
||||
function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
|
||||
sourceObj[sourceMethod] = function() {
|
||||
return targetObj[targetMethod].apply(targetObj, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
var utc = {
|
||||
date: d
|
||||
};
|
||||
|
||||
// support strftime, if found
|
||||
|
||||
if (d.strftime != undefined) {
|
||||
addProxyMethod(utc, "strftime", d, "strftime");
|
||||
}
|
||||
|
||||
addProxyMethod(utc, "getTime", d, "getTime");
|
||||
addProxyMethod(utc, "setTime", d, "setTime");
|
||||
|
||||
var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"];
|
||||
|
||||
for (var p = 0; p < props.length; p++) {
|
||||
addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
|
||||
addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
|
||||
}
|
||||
|
||||
return utc;
|
||||
};
|
||||
|
||||
// select time zone strategy. This returns a date-like object tied to the
|
||||
// desired timezone
|
||||
|
||||
function dateGenerator(ts, opts) {
|
||||
if (opts.timezone == "browser") {
|
||||
return new Date(ts);
|
||||
} else if (!opts.timezone || opts.timezone == "utc") {
|
||||
return makeUtcWrapper(new Date(ts));
|
||||
} else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
|
||||
var d = new timezoneJS.Date();
|
||||
// timezone-js is fickle, so be sure to set the time zone before
|
||||
// setting the time.
|
||||
d.setTimezone(opts.timezone);
|
||||
d.setTime(ts);
|
||||
return d;
|
||||
} else {
|
||||
return makeUtcWrapper(new Date(ts));
|
||||
}
|
||||
}
|
||||
|
||||
// map of app. size of time units in milliseconds
|
||||
|
||||
var timeUnitSize = {
|
||||
"second": 1000,
|
||||
"minute": 60 * 1000,
|
||||
"hour": 60 * 60 * 1000,
|
||||
"day": 24 * 60 * 60 * 1000,
|
||||
"month": 30 * 24 * 60 * 60 * 1000,
|
||||
"quarter": 3 * 30 * 24 * 60 * 60 * 1000,
|
||||
"year": 365.2425 * 24 * 60 * 60 * 1000
|
||||
};
|
||||
|
||||
// the allowed tick sizes, after 1 year we use
|
||||
// an integer algorithm
|
||||
|
||||
var baseSpec = [
|
||||
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
|
||||
[30, "second"],
|
||||
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
|
||||
[30, "minute"],
|
||||
[1, "hour"], [2, "hour"], [4, "hour"],
|
||||
[8, "hour"], [12, "hour"],
|
||||
[1, "day"], [2, "day"], [3, "day"],
|
||||
[0.25, "month"], [0.5, "month"], [1, "month"],
|
||||
[2, "month"]
|
||||
];
|
||||
|
||||
// we don't know which variant(s) we'll need yet, but generating both is
|
||||
// cheap
|
||||
|
||||
var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
|
||||
[1, "year"]]);
|
||||
var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
|
||||
[1, "year"]]);
|
||||
|
||||
function init(plot) {
|
||||
plot.hooks.processOptions.push(function (plot, options) {
|
||||
$.each(plot.getAxes(), function(axisName, axis) {
|
||||
|
||||
var opts = axis.options;
|
||||
|
||||
if (opts.mode == "time") {
|
||||
axis.tickGenerator = function(axis) {
|
||||
|
||||
var ticks = [];
|
||||
var d = dateGenerator(axis.min, opts);
|
||||
var minSize = 0;
|
||||
|
||||
// make quarter use a possibility if quarters are
|
||||
// mentioned in either of these options
|
||||
|
||||
var spec = (opts.tickSize && opts.tickSize[1] ===
|
||||
"quarter") ||
|
||||
(opts.minTickSize && opts.minTickSize[1] ===
|
||||
"quarter") ? specQuarters : specMonths;
|
||||
|
||||
if (opts.minTickSize != null) {
|
||||
if (typeof opts.tickSize == "number") {
|
||||
minSize = opts.tickSize;
|
||||
} else {
|
||||
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < spec.length - 1; ++i) {
|
||||
if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
|
||||
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
|
||||
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var size = spec[i][0];
|
||||
var unit = spec[i][1];
|
||||
|
||||
// special-case the possibility of several years
|
||||
|
||||
if (unit == "year") {
|
||||
|
||||
// if given a minTickSize in years, just use it,
|
||||
// ensuring that it's an integer
|
||||
|
||||
if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
|
||||
size = Math.floor(opts.minTickSize[0]);
|
||||
} else {
|
||||
|
||||
var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
|
||||
var norm = (axis.delta / timeUnitSize.year) / magn;
|
||||
|
||||
if (norm < 1.5) {
|
||||
size = 1;
|
||||
} else if (norm < 3) {
|
||||
size = 2;
|
||||
} else if (norm < 7.5) {
|
||||
size = 5;
|
||||
} else {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
size *= magn;
|
||||
}
|
||||
|
||||
// minimum size for years is 1
|
||||
|
||||
if (size < 1) {
|
||||
size = 1;
|
||||
}
|
||||
}
|
||||
|
||||
axis.tickSize = opts.tickSize || [size, unit];
|
||||
var tickSize = axis.tickSize[0];
|
||||
unit = axis.tickSize[1];
|
||||
|
||||
var step = tickSize * timeUnitSize[unit];
|
||||
|
||||
if (unit == "second") {
|
||||
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
|
||||
} else if (unit == "minute") {
|
||||
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
|
||||
} else if (unit == "hour") {
|
||||
d.setHours(floorInBase(d.getHours(), tickSize));
|
||||
} else if (unit == "month") {
|
||||
d.setMonth(floorInBase(d.getMonth(), tickSize));
|
||||
} else if (unit == "quarter") {
|
||||
d.setMonth(3 * floorInBase(d.getMonth() / 3,
|
||||
tickSize));
|
||||
} else if (unit == "year") {
|
||||
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
|
||||
}
|
||||
|
||||
// reset smaller components
|
||||
|
||||
d.setMilliseconds(0);
|
||||
|
||||
if (step >= timeUnitSize.minute) {
|
||||
d.setSeconds(0);
|
||||
}
|
||||
if (step >= timeUnitSize.hour) {
|
||||
d.setMinutes(0);
|
||||
}
|
||||
if (step >= timeUnitSize.day) {
|
||||
d.setHours(0);
|
||||
}
|
||||
if (step >= timeUnitSize.day * 4) {
|
||||
d.setDate(1);
|
||||
}
|
||||
if (step >= timeUnitSize.month * 2) {
|
||||
d.setMonth(floorInBase(d.getMonth(), 3));
|
||||
}
|
||||
if (step >= timeUnitSize.quarter * 2) {
|
||||
d.setMonth(floorInBase(d.getMonth(), 6));
|
||||
}
|
||||
if (step >= timeUnitSize.year) {
|
||||
d.setMonth(0);
|
||||
}
|
||||
|
||||
var carry = 0;
|
||||
var v = Number.NaN;
|
||||
var prev;
|
||||
|
||||
do {
|
||||
|
||||
prev = v;
|
||||
v = d.getTime();
|
||||
ticks.push(v);
|
||||
|
||||
if (unit == "month" || unit == "quarter") {
|
||||
if (tickSize < 1) {
|
||||
|
||||
// a bit complicated - we'll divide the
|
||||
// month/quarter up but we need to take
|
||||
// care of fractions so we don't end up in
|
||||
// the middle of a day
|
||||
|
||||
d.setDate(1);
|
||||
var start = d.getTime();
|
||||
d.setMonth(d.getMonth() +
|
||||
(unit == "quarter" ? 3 : 1));
|
||||
var end = d.getTime();
|
||||
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
|
||||
carry = d.getHours();
|
||||
d.setHours(0);
|
||||
} else {
|
||||
d.setMonth(d.getMonth() +
|
||||
tickSize * (unit == "quarter" ? 3 : 1));
|
||||
}
|
||||
} else if (unit == "year") {
|
||||
d.setFullYear(d.getFullYear() + tickSize);
|
||||
} else {
|
||||
d.setTime(v + step);
|
||||
}
|
||||
} while (v < axis.max && v != prev);
|
||||
|
||||
return ticks;
|
||||
};
|
||||
|
||||
axis.tickFormatter = function (v, axis) {
|
||||
|
||||
var d = dateGenerator(v, axis.options);
|
||||
|
||||
// first check global format
|
||||
|
||||
if (opts.timeformat != null) {
|
||||
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
|
||||
}
|
||||
|
||||
// possibly use quarters if quarters are mentioned in
|
||||
// any of these places
|
||||
|
||||
var useQuarters = (axis.options.tickSize &&
|
||||
axis.options.tickSize[1] == "quarter") ||
|
||||
(axis.options.minTickSize &&
|
||||
axis.options.minTickSize[1] == "quarter");
|
||||
|
||||
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
|
||||
var span = axis.max - axis.min;
|
||||
var suffix = (opts.twelveHourClock) ? " %p" : "";
|
||||
var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
|
||||
var fmt;
|
||||
|
||||
if (t < timeUnitSize.minute) {
|
||||
fmt = hourCode + ":%M:%S" + suffix;
|
||||
} else if (t < timeUnitSize.day) {
|
||||
if (span < 2 * timeUnitSize.day) {
|
||||
fmt = hourCode + ":%M" + suffix;
|
||||
} else {
|
||||
fmt = "%b %d " + hourCode + ":%M" + suffix;
|
||||
}
|
||||
} else if (t < timeUnitSize.month) {
|
||||
fmt = "%b %d";
|
||||
} else if ((useQuarters && t < timeUnitSize.quarter) ||
|
||||
(!useQuarters && t < timeUnitSize.year)) {
|
||||
if (span < timeUnitSize.year) {
|
||||
fmt = "%b";
|
||||
} else {
|
||||
fmt = "%b %Y";
|
||||
}
|
||||
} else if (useQuarters && t < timeUnitSize.year) {
|
||||
if (span < timeUnitSize.year) {
|
||||
fmt = "Q%q";
|
||||
} else {
|
||||
fmt = "Q%q %Y";
|
||||
}
|
||||
} else {
|
||||
fmt = "%Y";
|
||||
}
|
||||
|
||||
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
|
||||
|
||||
return rt;
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$.plot.plugins.push({
|
||||
init: init,
|
||||
options: options,
|
||||
name: 'time',
|
||||
version: '1.0'
|
||||
});
|
||||
|
||||
// Time-axis support used to be in Flot core, which exposed the
|
||||
// formatDate function on the plot object. Various plugins depend
|
||||
// on the function, so we need to re-expose it here.
|
||||
|
||||
$.plot.formatDate = formatDate;
|
||||
$.plot.dateGenerator = dateGenerator;
|
||||
|
||||
})(jQuery);
|
|
@ -10,6 +10,7 @@ require.config({
|
|||
text : '3rdparty/require/text',
|
||||
flot : '3rdparty/flot/jquery.flot',
|
||||
flotresize : '3rdparty/flot/jquery.flot.resize',
|
||||
flottime : '3rdparty/flot/jquery.flot.time',
|
||||
fgcommand : 'lib/fgcommand',
|
||||
}
|
||||
});
|
||||
|
@ -56,8 +57,8 @@ require([
|
|||
c.forEach(function(e) {
|
||||
self.addListener(e.prop, e.koObservable);
|
||||
});
|
||||
for( var p in self.listeners ) {
|
||||
self.addListener( p, self.listeners[p] );
|
||||
for ( var p in self.listeners) {
|
||||
self.addListener(p, self.listeners[p]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,6 +75,16 @@ require([
|
|||
|
||||
self.listeners = {}
|
||||
|
||||
self.getListener = function(pathOrAlias) {
|
||||
if( pathOrAlias in self.listeners ) {
|
||||
return self.listeners[pathOrAlias];
|
||||
}
|
||||
}
|
||||
|
||||
self.removeListener = function(pathOrAlias) {
|
||||
|
||||
}
|
||||
|
||||
self.addListener = function(alias, koObservable) {
|
||||
if (self.openCache) {
|
||||
// socket not yet open, just cache the request
|
||||
|
@ -292,7 +303,7 @@ require([
|
|||
}
|
||||
|
||||
self.selectTopic(self.topics[0]);
|
||||
|
||||
|
||||
self.refresh = function() {
|
||||
location.reload();
|
||||
}
|
||||
|
@ -338,6 +349,30 @@ require([
|
|||
require : 'widgets/Stopwatch'
|
||||
});
|
||||
|
||||
ko.bindingHandlers.flotchart = {
|
||||
init : function(element, valueAccessor, allBindings) {
|
||||
// This will be called when the binding is first applied to an
|
||||
// element
|
||||
// Set up any initial state, event handlers, etc. here
|
||||
var value = valueAccessor() || {};
|
||||
|
||||
if (value.hover && typeof (value.hover) === 'function') {
|
||||
$(element).bind("plothover", function(event, pos, item) {
|
||||
value.hover(pos, item);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
update : function(element, valueAccessor, allBindings) {
|
||||
var value = valueAccessor() || {};
|
||||
var data = ko.unwrap( value.data );
|
||||
var options = ko.unwrap( value.options );
|
||||
jquery.plot(element, data, options );
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
ko.applyBindings(new PhiViewModel());
|
||||
|
||||
});
|
||||
|
|
|
@ -2,57 +2,6 @@ define([
|
|||
'jquery', 'knockout', 'text!./MassBalance.html', 'flot', 'kojqui/slider', 'flotresize'
|
||||
], function(jquery, ko, htmlString) {
|
||||
|
||||
ko.bindingHandlers.flotchart = {
|
||||
init : function(element, valueAccessor, allBindings) {
|
||||
// This will be called when the binding is first applied to an
|
||||
// element
|
||||
// Set up any initial state, event handlers, etc. here
|
||||
var value = valueAccessor() || {};
|
||||
|
||||
var plot = jquery.plot(element, []);
|
||||
ko.utils.domData.set(element, "flotchart-plot", plot);
|
||||
|
||||
if (value.hover && typeof (value.hover) === 'function') {
|
||||
$(element).bind("plothover", function(event, pos, item) {
|
||||
value.hover(pos, item);
|
||||
});
|
||||
}
|
||||
|
||||
if (ko.isObservable(value.options)) {
|
||||
value.options.subscribe(function(newValue) {
|
||||
var element = this;
|
||||
// options changed - start with a new plot, reuse data
|
||||
var plot = ko.utils.domData.get(element, "flotchart-plot");
|
||||
plot = jquery.plot(element, plot.getData(), newValue);
|
||||
ko.utils.domData.set(element, "flotchart-plot", plot);
|
||||
}, element);
|
||||
}
|
||||
|
||||
if (ko.isObservable(value.data)) {
|
||||
value.data.subscribe(function(newValue) {
|
||||
var element = this;
|
||||
|
||||
var plot = ko.utils.domData.get(element, "flotchart-plot");
|
||||
plot.setData(newValue);
|
||||
// TODO: setupGrid not always required
|
||||
plot.setupGrid();
|
||||
plot.draw();
|
||||
|
||||
}, element);
|
||||
}
|
||||
|
||||
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
|
||||
// This will be called when the element is removed by Knockout
|
||||
// or
|
||||
// if some other part of your code calls ko.removeNode(element)
|
||||
var plot = ko.utils.domData.set(element, "flotchart-plot", null);
|
||||
// TODO: unsubscribe from data and options observables!!
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
function ViewModel(params) {
|
||||
var self = this;
|
||||
|
||||
|
|
|
@ -15,17 +15,23 @@
|
|||
}
|
||||
|
||||
.property-value {
|
||||
padding: 0 0.2em;
|
||||
min-width: 8em;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
padding: 0 0.2em;
|
||||
min-width: 8em;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
<div class="ui-widget ui-widget-content ui-corner-all">
|
||||
<div class="ui-widget-header">Property Tree</div>
|
||||
<div data-bind="template: { name: 'propertytree-template', data: properties }"></div>
|
||||
<div class="ui-widget ui-widget-content ui-corner-all" data-bind="visible: hasGraphItems">
|
||||
<div class="ui-widget-header">Property Graph</div>
|
||||
<div style="height: 300px; width: 100%"
|
||||
data-bind="flotchart: { data: flotData, options: flotOptions, hover: graphHover }"></div>
|
||||
</div>
|
||||
<script type="text/html" id="propertytree-template">
|
||||
<div class=" ui-widget
|
||||
ui-widget-contentui-corner-all">
|
||||
<div class="ui-widget-header">Property Tree</div>
|
||||
<div data-bind="template: { name: 'propertytree-template', data: properties }"></div>
|
||||
</div>
|
||||
<script type="text/html" id="propertytree-template">
|
||||
<ul class="property-list" data-bind="foreach: $data">
|
||||
<li>
|
||||
<span class="ui-icon pointer-icon" style="display: inline-block;"
|
||||
|
@ -34,12 +40,14 @@
|
|||
'ui-icon-triangle-1-e': hasChildren,
|
||||
'ui-icon-triangle-1-se': isExpanded,
|
||||
'ui-icon-refresh': hasValue,
|
||||
'ui-icon-blank': !(hasValue||hasChildren),
|
||||
},
|
||||
attr: {
|
||||
title: hasValue ? 'click to refresh' : 'click to expand/collapse',
|
||||
title: hasValue ? 'refresh' : 'expand/collapse',
|
||||
},
|
||||
click: toggle,
|
||||
"></span>
|
||||
|
||||
<span class="property-name"
|
||||
data-bind="
|
||||
text: indexedName,
|
||||
|
@ -49,6 +57,17 @@
|
|||
click: toggle,
|
||||
"></span>
|
||||
|
||||
<span class="ui-icon ui-icon-blank" style="display: inline-block;"
|
||||
data-bind="
|
||||
css: {
|
||||
'ui-icon-image': isPlottable,
|
||||
'pointer-icon': hasValue,
|
||||
},
|
||||
attr: {
|
||||
title: hasValue ? 'toggle plot' : '',
|
||||
},
|
||||
click: togglePlot,
|
||||
"></span>
|
||||
<span class="property-value ui-state-hover pointer-icon ui-corner-all"
|
||||
data-bind="
|
||||
text: value,
|
||||
|
@ -66,6 +85,6 @@
|
|||
</li>
|
||||
</ul>
|
||||
</script>
|
||||
<script type="text/html" id="inplace-editor-template">
|
||||
<script type="text/html" id="inplace-editor-template">
|
||||
<input style="width: 8em" >
|
||||
</script>
|
||||
</script>
|
|
@ -1,14 +1,81 @@
|
|||
define([
|
||||
'jquery', 'knockout', 'text!./Properties.html',
|
||||
'jquery', 'knockout', 'text!./Properties.html', 'flot', 'flotresize', 'flottime'
|
||||
], function(jquery, ko, htmlString) {
|
||||
|
||||
function PropertyViewModel() {
|
||||
function SampleSource(prop, source, params) {
|
||||
params = params || {};
|
||||
|
||||
this.source = source;
|
||||
this.path = prop.path;
|
||||
this.maxSamples = params.maxSamples || 100;
|
||||
|
||||
this.samples = [];
|
||||
this.sample = function(timeStamp) {
|
||||
while (this.samples.length >= this.maxSamples)
|
||||
this.samples.shift();
|
||||
this.samples.push([
|
||||
timeStamp, this.source()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function PropertySampler(params) {
|
||||
|
||||
params = params || {};
|
||||
|
||||
this.sources = {};
|
||||
this.sampleInterval = params.sampleInterval || 1000;
|
||||
|
||||
this.start = function() {
|
||||
this.update(++this.updateId);
|
||||
return this;
|
||||
}
|
||||
|
||||
this.stop = function() {
|
||||
++this.updateId;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.addSource = function(source) {
|
||||
this.sources[source.path] = source;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.removeSource = function(source) {
|
||||
delete this.sources[source];
|
||||
return this;
|
||||
}
|
||||
|
||||
this.containsSource = function(source) {
|
||||
return source in this.sources;
|
||||
}
|
||||
|
||||
this.updateId = 0;
|
||||
this.update = function(id) {
|
||||
if (id != this.updateId)
|
||||
return;
|
||||
|
||||
var now = Date.now();
|
||||
for ( var key in this.sources) {
|
||||
this.sources[key].sample(now);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.update(id);
|
||||
}, self.sampleInterval);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function PropertyViewModel(propertyPlotter) {
|
||||
var self = this;
|
||||
|
||||
function load() {
|
||||
jquery.get('/json' + self.path, null, function(data) {
|
||||
self.hasChildren = data.nChildren > 0;
|
||||
self.index = data.index;
|
||||
self.type = data.type;
|
||||
if (typeof (data.value) != 'undefined') {
|
||||
self.value(data.value);
|
||||
self.hasValue = true;
|
||||
|
@ -20,10 +87,11 @@ define([
|
|||
var a = [];
|
||||
if (data.children) {
|
||||
data.children.forEach(function(prop) {
|
||||
var p = new PropertyViewModel();
|
||||
var p = new PropertyViewModel(propertyPlotter);
|
||||
p.name = prop.name;
|
||||
p.path = prop.path;
|
||||
p.index = prop.index;
|
||||
p.type = prop.type;
|
||||
p.hasChildren = prop.nChildren > 0;
|
||||
if (typeof (prop.value) != 'undefined') {
|
||||
p.value(prop.value);
|
||||
|
@ -50,10 +118,12 @@ define([
|
|||
self.path = '';
|
||||
self.hasChildren = false;
|
||||
self.hasValue = false;
|
||||
self.type = '';
|
||||
|
||||
self.indexedName = ko.pureComputed(function() {
|
||||
if( 0 == self.index ) return self.name;
|
||||
return self.name + "[" + self.index + "]";
|
||||
self.indexedName = ko.pureComputed(function() {
|
||||
if (0 == self.index)
|
||||
return self.name;
|
||||
return self.name + "[" + self.index + "]";
|
||||
});
|
||||
|
||||
self.isExpanded = ko.observable(false);
|
||||
|
@ -65,6 +135,12 @@ define([
|
|||
}
|
||||
});
|
||||
|
||||
self.isPlottable = ko.pureComputed(function() {
|
||||
return [
|
||||
"double", "float", "int"
|
||||
].indexOf(self.type) != -1;
|
||||
});
|
||||
|
||||
self.toggle = function() {
|
||||
if (self.hasChildren) {
|
||||
self.isExpanded(!self.isExpanded());
|
||||
|
@ -73,6 +149,10 @@ define([
|
|||
}
|
||||
}
|
||||
|
||||
self.togglePlot = function(prop, evt) {
|
||||
propertyPlotter.toggleProp(prop);
|
||||
}
|
||||
|
||||
self.valueEdit = function(prop, evt) {
|
||||
var inplaceEditor = jquery(jquery('#inplace-editor-template').html());
|
||||
|
||||
|
@ -116,15 +196,104 @@ define([
|
|||
function ViewModel(params) {
|
||||
var self = this;
|
||||
|
||||
self.root = new PropertyViewModel();
|
||||
self.root = new PropertyViewModel(self);
|
||||
self.root.name = "root";
|
||||
self.root.path = "/";
|
||||
self.root.isExpanded(true);
|
||||
self.properties = self.root.children;
|
||||
|
||||
self.flotOptions = ko.observable({
|
||||
xaxes : [
|
||||
{
|
||||
mode : "time"
|
||||
}
|
||||
],
|
||||
yaxes : [
|
||||
{
|
||||
position : "right"
|
||||
}, {
|
||||
position : "left"
|
||||
}
|
||||
|
||||
],
|
||||
legend : {
|
||||
show : false,
|
||||
},
|
||||
grid : {
|
||||
hoverable : true,
|
||||
}
|
||||
});
|
||||
|
||||
self.flotData = ko.observableArray([]);
|
||||
|
||||
self.graphHover = function() {
|
||||
}
|
||||
|
||||
self.hasGraphItems = ko.pureComputed(function() {
|
||||
return self.flotData().length > 0;
|
||||
});
|
||||
|
||||
self.propertySampler = new PropertySampler({
|
||||
sampleInterval : 100,
|
||||
});
|
||||
|
||||
self.propertySampler.start();
|
||||
|
||||
self.toggleProp = function(prop) {
|
||||
|
||||
if (self.propertySampler.containsSource(prop.path)) {
|
||||
self.propertySampler.removeSource(prop.path);
|
||||
return;
|
||||
}
|
||||
|
||||
var obs = ko.utils.knockprops.getListener(prop.path);
|
||||
if (obs) {
|
||||
self.propertySampler.addSource(new SampleSource(prop, obs, {
|
||||
maxSamples : 1000,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
self.update = function() {
|
||||
|
||||
var sources = self.propertySampler.sources;
|
||||
var data = [];
|
||||
|
||||
var i = 1;
|
||||
for ( var key in sources) {
|
||||
var source = sources[key];
|
||||
data.push({
|
||||
// color : 'rgb(192, 128, 0)',
|
||||
data : source.samples,
|
||||
label : key,
|
||||
lines : {
|
||||
show : true
|
||||
},
|
||||
points : {
|
||||
show : false
|
||||
},
|
||||
bars : {
|
||||
show : false
|
||||
},
|
||||
shadowSize : 0,
|
||||
yaxis: i++,
|
||||
});
|
||||
}
|
||||
|
||||
self.flotData(data);
|
||||
|
||||
setTimeout(function() {
|
||||
self.update();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
self.update();
|
||||
}
|
||||
|
||||
// ViewModel.prototype.dispose = function() {
|
||||
// }
|
||||
ViewModel.prototype.dispose = function() {
|
||||
console.log("disposing pal");
|
||||
this.propertySampler.stop();
|
||||
}
|
||||
|
||||
// Return component definition
|
||||
return {
|
||||
|
|
Loading…
Add table
Reference in a new issue