Metric Displays

OVMS V3 is based on metrics. Metrics can be single numerical or textual values or complex values like sets and arrays. The web framework keeps all metrics in a global object, which can be read simply by e.g. metrics["v.b.soc"].

In addition to the raw metric values, there are 2 main proxy arrays that give access to the user-configured versions of the raw metric values. The metrics_user array converts the ‘metrics’ value to user-configured value and the metrics_label array provides the corresponding label for that metric. So for example the user could configure distance values to be in miles, and in this case metrics["v.p.odometer"] would still contain the value in km (the default) but metrics_user["v.p.odometer"] would give the value converted to miles and metrics_label["v.p.odometer"] would return “M”.

The user conversion information is contained in another object units. units.metrics has the user configuration for each metric and units.prefs has the user configuration for each group of metrics (distance, temperature, consumption, pressure etc). There also some methods for general conversions allowing user preferences:

  • The method units.unitLabelToUser(unitType,name) will return the user defined label for that ‘unitType’, defaulting to name.

  • The method units.unitValueToUser(unitType,value) will convert value to the user defined unit (if set) for the group.

Metrics updates (as well as other updates) are sent to all DOM elements having the receiver class. To hook into these updates, simply add an event listener for msg:metrics:. The event msg:units:metrics is called when units.metrics is change and msg:units:prefs when units.prefs are changed.

Listening to the event is not necessary though if all you need is some metrics display. This is covered by the metric widget class family as shown here.

Single Values & Charts

../../../_images/metrics.png

The following example covers…

  • Text (String) displays

  • Number displays

  • Progress bars (horizontal light weight bar charts)

  • Gauges

  • Charts

Where a number element of class ‘metric’ contains both elements of class ‘value’ and ‘unit’, these will be automatically displayed in the units selected in the user preferences. Having a ‘data-user’ attribute will also cause the ‘value’ element to be displayed in user units (unless ‘data-scale’ attribute is present).

Gauges & charts use the HighCharts library, which is included in the web server. The other widgets are simple standard Bootstrap widgets extended by an automatic metrics value update mechanism.

Highcharts is a highly versatile charting system. For inspiration, have a look at:

We’re using styled mode so some options don’t apply, but everything can be styled by standard CSS.

Install the example as a web page plugin:

metrics.htm (hint: right click, save as)

  1<!--
  2  Test/Development/Documentation page; install as plugin to test
  3-->
  4
  5<div class="panel panel-primary">
  6  <div class="panel-heading">Metrics Displays Test/Demo</div>
  7  <div class="panel-body">
  8
  9    <p>OVMS V3 is based on metrics. Metrics can be single numerical or textual values or complex values
 10      like sets and arrays. The web framework keeps all metrics in a global object, which can be read
 11      simply by e.g. <code>metrics["v.b.soc"]</code>.</p>
 12
 13    <p>Metrics updates (as well as other updates) are sent to all DOM elements having the
 14      <code>receiver</code> class. To hook into these updates, simply add an event listener for
 15      <code>msg:metrics</code>. Listening to the event is not necessary if all you need is some metrics
 16      display. This is covered by the <code>metric</code> class family as shown here.</p>
 17
 18    <p>
 19      <button type="button" class="btn btn-default action-gendata">Generate random data</button>
 20      <button type="button" class="btn btn-default action-showsrc">Show page source</button>
 21    </p>
 22
 23    <hr/>
 24
 25    <div class="receiver">
 26
 27      <h4>Basic usage</h4>
 28
 29      <p>All elements of class <code>metric</code> in a <code>receiver</code> are checked for the
 30        <code>data-metric</code> attribute. If no specific metric class is given, the metric value
 31        is simply set as the element text: <span class="metric" data-metric="m.net.provider">?</span>
 32        is your current network provider.</p>
 33
 34      <h4>Text &amp; Number</h4>
 35
 36      <p><code>number</code> &amp; <code>text</code> displays get the metric value set in their child of
 37        class <code>value</code>. They may additionally have labels and units. <code>data-prec</code> can
 38        be used on <code>number</code> to set the precision, <code>data-scale</code> to scale the raw
 39        values by a factor. They have fixed min widths and float by default, so you can simply put
 40        multiple displays into the same container:</p>
 41
 42      <div class="clearfix">
 43        <div class="metric number" data-metric="v.e.throttle" data-prec="0">
 44          <span class="label">Throttle:</span>
 45          <span class="value">?</span>
 46          <span class="unit">%</span>
 47        </div>
 48        <div class="metric number" data-metric="v.b.12v.voltage.ref" data-prec="1">
 49          <span class="value">?</span>
 50          <span class="unit">V<sub>ref</sub></span>
 51        </div>
 52        <div class="metric text" data-metric="m.net.provider">
 53          <span class="label">Network:</span>
 54          <span class="value">?</span>
 55        </div>
 56      </div>
 57
 58      <h4>Derived Values</h4>
 59
 60      <p>If the <code>metric</code> element has a <code>data-template</code> attribute, its content is
 61        interpreted as a Javascript
 62        <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals">
 63        ES6 template string</a>. The interpolation result replaces the text content of either the element's
 64        children with class <code>value</code>, or if none are present of the element itself.</p>
 65
 66      <p>This can be used to display values calculated or combined from multiple metrics, or apply
 67        simple custom value formatting and code translations without the need to implement a custom
 68        reception handler.</p>
 69
 70      <p>Example: derive 12V power as voltage &times; current:</p>
 71
 72      <div class="clearfix">
 73        <div class="metric number" data-metric="v.b.12v.voltage,v.b.12v.current"
 74          data-template="${(metrics['v.b.12v.voltage'] * (metrics['v.b.12v.current']||0.1)).toFixed(2)}">
 75          <span class="label">12V Power:</span>
 76          <span class="value"></span>
 77          <span class="unit">W</span>
 78        </div>
 79      </div>
 80
 81      <h4>Progress Bar</h4>
 82
 83      <p>Bootstrap <code>progress</code> bars can be used as lightweight graphical indicators.
 84        Labels and units are available, also <code>data-prec</code> and <code>data-scale</code>.
 85        Again, all you need is a bit of markup:</p>
 86
 87      <div class="clearfix">
 88        <div class="metric progress" data-metric="v.e.throttle" data-prec="0">
 89          <div class="progress-bar progress-bar-success value-low text-left" role="progressbar"
 90            aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:0%">
 91            <div>
 92              <span class="label">Throttle:</span>
 93              <span class="value">?</span>
 94              <span class="unit">%</span>
 95            </div>
 96          </div>
 97        </div>
 98        <div class="metric progress" data-metric="v.b.12v.voltage.ref" data-prec="1">
 99          <div class="progress-bar progress-bar-info value-low text-left" role="progressbar"
100            aria-valuenow="0" aria-valuemin="5" aria-valuemax="15" style="width:0%">
101            <div>
102              <span class="label">12V ref:</span>
103              <span class="value">?</span>
104              <span class="unit">V</span>
105            </div>
106          </div>
107        </div>
108      </div>
109  
110      <h4>Gauges &amp; Charts</h4>
111
112      <p>The OVMS web framework has builtin support for the highly versatile <b>Highcharts library</b>
113        with loads of chart types and options. <code>chart</code> metric examples:</p>
114
115      <div class="row">
116        <div class="col-sm-6">
117          <div class="metric chart" data-metric="v.e.throttle" style="height:220px">
118            <div class="chart-box gaugechart" id="throttle-gauge"/>
119          </div>
120        </div>
121        <div class="col-sm-6">
122          <div class="metric chart" data-metric="v.b.c.voltage,v.b.c.voltage.min" style="height:220px">
123            <div class="chart-box barchart" id="cell-voltages"/>
124          </div>
125        </div>
126      </div>
127
128      <p><button type="button" class="btn btn-default action-gendata">Generate random data</button></p>
129
130      <p>For charts, a little bit of scripting is necessary.
131        The scripts for these charts contain the chart configuration, part of which is the update
132        function you need to define. The update function translates metrics data into chart data.
133        This is trivial for single values like the throttle, the cell voltage chart is an example
134        on basic array processing.</p>
135
136      <p>Also, while charts <em>can</em> be defined with few options, you'll <em>love</em> to explore
137        all the features and fine tuning options provided by Highcharts. For inspiration,
138        have a look at the <a target="_blank" href="https://www.highcharts.com/demo">Highcharts demos</a>
139        and the <a target="_blank" href="https://www.highcharts.com/docs/">Highcharts documentation</a>.
140        We're using <a target="_blank" href="https://www.highcharts.com/docs/chart-design-and-style/style-by-css">
141        styled mode</a>, so some options don't apply, but everything can be styled by standard CSS.</p>
142
143    </div>
144
145  </div>
146</div>
147
148<script>
149(function(){
150
151  /* Get page source before chart rendering: */
152  var pagesrc = $('#main').html();
153
154  /* Init throttle gauge: */
155  $("#throttle-gauge").chart({
156    chart: {
157      type: 'gauge',
158      spacing: [0, 0, 0, 0],
159      margin: [0, 0, 0, 0],
160      animation: { duration: 500, easing: 'easeOutExpo' },
161    },
162    title: { text: "Throttle", verticalAlign: "middle", y: 75 },
163    credits: { enabled: false },
164    tooltip: { enabled: false },
165    plotOptions: {
166      gauge: { dataLabels: { enabled: false }, overshoot: 1 }
167    },
168    pane: [{
169      startAngle: -125, endAngle: 125, size: '100%', center: ['50%', '60%']
170    }],
171    yAxis: [{
172      title: { text: '%' },
173      className: 'throttle',
174      reversed: false,
175      min: 0, max: 100,
176      plotBands: [
177        { from: 0, to: 60, className: 'green-band' },
178        { from: 60, to: 80, className: 'yellow-band' },
179        { from: 80, to: 100, className: 'red-band' },
180      ],
181      minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',
182      tickPixelInterval: 40, tickPosition: 'inside', tickLength: 13,
183      labels: { step: 2, distance: -28, x: 0, y: 5, zIndex: 2 },
184    }],
185    series: [{
186      name: 'Throttle', data: [0],
187      className: 'throttle',
188      animation: { duration: 0 },
189      pivot: { radius: '10' },
190      dial: { radius: '88%', topWidth: 1, baseLength: '20%', baseWidth: 10, rearLength: '20%' },
191    }],
192    /* Update method: */
193    onUpdate: function(update) {
194      // Create gauge data set from metric:
195      var data = [ metrics["v.e.throttle"] ];
196      // Update chart:
197      this.series[0].setData(data);
198    },
199  });
200
201  /* Init cell voltages chart */
202  $("#cell-voltages").chart({
203    chart: {
204      type: 'column',
205      animation: { duration: 500, easing: 'easeOutExpo' },
206    },
207    title: { text: "Cell Voltages" },
208    credits: { enabled: false },
209    tooltip: {
210      enabled: true,
211      shared: true,
212      headerFormat: 'Cell #{point.key}:<br/>',
213      pointFormat: '{series.name}: <b>{point.y}</b><br/>',
214      valueSuffix: " V"
215    },
216    legend: { enabled: true },
217    xAxis: {
218      categories: []
219    },
220    yAxis: [{
221      title: { text: null },
222      labels: { format: "{value:.2f}V" },
223      tickAmount: 4, startOnTick: false, endOnTick: false,
224      floor: 3.3, ceiling: 4.2,
225      minorTickInterval: 'auto',
226    }],
227    series: [{
228      name: 'Current', data: [],
229      className: 'cell-voltage',
230      animation: { duration: 0 },
231    },{
232      name: 'Minimum', data: [],
233      className: 'cell-voltage-min',
234      animation: { duration: 0 },
235    }],
236    /* Update method: */
237    onUpdate: function(update) {
238      // Note: the 'update' parameter contains the actual update set.
239      // You can use this to reduce chart updates to the actual changes.
240      // For this demo, we just use the global metrics object:
241      var
242        m_vlt = metrics["v.b.c.voltage"] || [],
243        m_min = metrics["v.b.c.voltage.min"] || [];
244      // Create categories (cell numbers) & rounded values:
245      var cat = [], val0 = [], val1 = [];
246      for (var i = 0; i < m_vlt.length; i++) {
247        cat.push(i+1);
248        val0.push(Number((m_vlt[i]||0).toFixed(3)));
249        val1.push(Number((m_min[i]||0).toFixed(3)));
250      }
251      // Update chart:
252      this.xAxis[0].setCategories(cat);
253      this.series[0].setData(val0);
254      this.series[1].setData(val1);
255    },
256  });
257
258  /* Test metrics generator: */
259  $('.action-gendata').on('click', function() {
260    var td = {};
261    td["m.net.provider"] = ["hologram","Vodafone","Telekom"][Math.floor(Math.random()*3)];
262    td["v.e.throttle"] = Math.random() * 100;
263    td["v.b.12v.voltage.ref"] = 10 + Math.random() * 4;
264    var m_vlt = [], m_min = [];
265    for (var i = 1; i <= 16; i++) {
266      m_vlt.push(3.6 + Math.random() * 0.5);
267      m_min.push(3.4 + Math.random() * 0.2);
268    }
269    td["v.b.c.voltage"] = m_vlt;
270    td["v.b.c.voltage.min"] = m_min;
271    $('.receiver').trigger('msg:metrics', $.extend(metrics, td));
272  });
273
274  /* Display page source: */
275  $('.action-showsrc').on('click', function() {
276    $('<div/>').dialog({
277      title: 'Source Code',
278      body: '<pre style="font-size:85%; height:calc(100vh - 230px);">'
279        + encode_html(pagesrc) + '</pre>',
280      size: 'lg',
281    });
282  });
283
284})();
285</script>

Vector Tables

../../../_images/metrics-table.png

Some metrics, for example the battery cell voltages or the TPMS tyre health data, may contain vectors of arbitrary size. Besides rendering into charts, these can also be displayed by their textual values in form of a table.

The following example shows a live view of the battery cell voltages along with their recorded minimum, maximum, maximum deviation and current warning/alert state. Alert states 0-2 are translated into icons.

The metric table widget uses the DataTables library, which is included in the web server. The DataTables Javascript library offers a wide range of options to create tabular views into datasets.

Install the example as a web page plugin:

metrics-table.htm (hint: right click, save as)

  1<!--
  2  Web UI page plugin: DataTables metrics widget demonstration
  3-->
  4
  5<style>
  6td i {
  7  font-style: normal;
  8  font-size: 140%;
  9  line-height: 90%;
 10  font-weight: bold;
 11}
 12td i.warning { color: orange; }
 13td i.danger { color: red; }
 14</style>
 15
 16
 17<div class="panel panel-primary panel-single receiver" id="my-receiver">
 18  <div class="panel-heading">Metrics Table Widget Example</div>
 19  <div class="panel-body">
 20
 21    <p>The following table shows a live view of the battery cell voltages along with their recorded
 22      minimum, maximum, maximum deviation and current warning/alert state.</p>
 23    <p>Try resizing the window or using a mobile phone to see how the table adapts to the screen
 24      width. The table will also keep the selected sorting over data updates.</p>
 25    <p>Hint: if you don't have live battery cell data, click the generator button to create
 26      some random values. The random data is only generated in your browser, not on the module.</p>
 27
 28    <div class="metric table"
 29      data-metric="v.b.c.voltage,v.b.c.voltage.min,v.b.c.voltage.max,v.b.c.voltage.dev.max,v.b.c.voltage.alert">
 30      <table class="table table-striped table-bordered table-hover" id="v-table" />
 31    </div>
 32
 33    <p>See <a target="_blank" href="https://datatables.net/manual/">DataTables manual</a> for all
 34      options and API methods available.</p>
 35
 36  </div>
 37  <div class="panel-footer">
 38    <p><button type="button" class="btn btn-default action-gendata">Generate random data</button></p>
 39  </div>
 40</div>
 41
 42
 43<script>
 44(function(){
 45
 46  // Utilities:
 47  var alertMap = {
 48    0: '',
 49    1: '<i class="warning">⚐</i>',
 50    2: '<i class="danger">⚑</i>',
 51  };
 52
 53  function fmtCode(value, map) {
 54    return (map[value] !== undefined) ? map[value] : null;
 55  }
 56  function fmtNumber(value, prec) {
 57    return (value !== undefined) ? Number(value).toFixed(prec) : null;
 58  }
 59
 60  // Init table:
 61  $('#v-table').table({
 62    responsive: true,
 63    paging: true,
 64    searching: false,
 65    info: false,
 66    autoWidth: false,
 67    columns: [
 68      { title: "#",         className: "dt-body-center",  width: "6%",  responsivePriority: 1 },
 69      { title: "Voltage",   className: "dt-body-right",   width: "22%", responsivePriority: 3 },
 70      { title: "Minimum",   className: "dt-body-right",   width: "22%", responsivePriority: 4 },
 71      { title: "Maximum",   className: "dt-body-right",   width: "22%", responsivePriority: 5 },
 72      { title: "Max.Dev.",  className: "dt-body-right",   width: "22%", responsivePriority: 2 },
 73      { title: "Alert",     className: "dt-body-center",  width: "6%",  responsivePriority: 1 },
 74    ],
 75    rowId: 0,
 76    onUpdate: function(update) {
 77      // Get vector metrics to display:
 78      var v = [
 79        metrics["v.b.c.voltage"] || [],
 80        metrics["v.b.c.voltage.min"] || [],
 81        metrics["v.b.c.voltage.max"] || [],
 82        metrics["v.b.c.voltage.dev.max"] || [],
 83        metrics["v.b.c.voltage.alert"] || [],
 84      ];
 85      var lcnt = 0;
 86      v.map(el => lcnt = Math.max(lcnt, el.length));
 87      // Transpose vectors to columns:
 88      var l, d = [];
 89      for (l = 0; l < lcnt; l++) {
 90        d.push([
 91          l+1,
 92          fmtNumber(v[0][l], 2),
 93          fmtNumber(v[1][l], 2),
 94          fmtNumber(v[2][l], 2),
 95          fmtNumber(v[3][l], 3),
 96          fmtCode(v[4][l], alertMap),
 97        ]);
 98      }
 99      // Display new data:
100      this.clear().rows.add(d).draw();
101    },
102  });
103
104
105  // Test data generator:
106  $('.action-gendata').on('click', function() {
107    var td = {};
108    var m_vlt = [], m_min = [], m_max = [], m_devmax = [], m_alert = [];
109    for (var i = 1; i <= 16; i++) {
110      m_vlt.push(3.6 + Math.random() * 0.5);
111      m_min.push(3.4 + Math.random() * 0.2);
112      m_max.push(3.8 + Math.random() * 0.2);
113      m_devmax.push(-0.2 + Math.random() * 0.4);
114      m_alert.push(Math.floor(Math.random() * 3));
115    }
116    td["v.b.c.voltage"] = m_vlt;
117    td["v.b.c.voltage.min"] = m_min;
118    td["v.b.c.voltage.max"] = m_max;
119    td["v.b.c.voltage.dev.max"] = m_devmax;
120    td["v.b.c.voltage.alert"] = m_alert;
121    $('.receiver').trigger('msg:metrics', $.extend(metrics, td));
122  });
123
124})();
125</script>