1
0
Fork 0

Phi: property browser can do charts

This commit is contained in:
Torsten Dreyer 2015-03-01 18:22:56 +01:00
parent 15c6160c05
commit 59e0fa180a
5 changed files with 678 additions and 74 deletions

432
webgui/3rdparty/flot/jquery.flot.time.js vendored Normal file
View 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);

View file

@ -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());
});

View file

@ -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;

View file

@ -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>

View file

@ -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 {