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>Progress Bar</h4>
 59
 60      <p>Bootstrap <code>progress</code> bars can be used as lightweight graphical indicators.
 61        Labels and units are available, also <code>data-prec</code> and <code>data-scale</code>.
 62        Again, all you need is a bit of markup:</p>
 63
 64      <div class="clearfix">
 65        <div class="metric progress" data-metric="v.e.throttle" data-prec="0">
 66          <div class="progress-bar progress-bar-success value-low text-left" role="progressbar"
 67            aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:0%">
 68            <div>
 69              <span class="label">Throttle:</span>
 70              <span class="value">?</span>
 71              <span class="unit">%</span>
 72            </div>
 73          </div>
 74        </div>
 75        <div class="metric progress" data-metric="v.b.12v.voltage.ref" data-prec="1">
 76          <div class="progress-bar progress-bar-info value-low text-left" role="progressbar"
 77            aria-valuenow="0" aria-valuemin="5" aria-valuemax="15" style="width:0%">
 78            <div>
 79              <span class="label">12V ref:</span>
 80              <span class="value">?</span>
 81              <span class="unit">V</span>
 82            </div>
 83          </div>
 84        </div>
 85      </div>
 86  
 87      <h4>Gauges &amp; Charts</h4>
 88
 89      <p>The OVMS web framework has builtin support for the highly versatile <b>Highcharts library</b>
 90        with loads of chart types and options. <code>chart</code> metric examples:</p>
 91
 92      <div class="row">
 93        <div class="col-sm-6">
 94          <div class="metric chart" data-metric="v.e.throttle" style="height:220px">
 95            <div class="chart-box gaugechart" id="throttle-gauge"/>
 96          </div>
 97        </div>
 98        <div class="col-sm-6">
 99          <div class="metric chart" data-metric="v.b.c.voltage,v.b.c.voltage.min" style="height:220px">
100            <div class="chart-box barchart" id="cell-voltages"/>
101          </div>
102        </div>
103      </div>
104
105      <p><button type="button" class="btn btn-default action-gendata">Generate random data</button></p>
106
107      <p>For charts, a little bit of scripting is necessary.
108        The scripts for these charts contain the chart configuration, part of which is the update
109        function you need to define. The update function translates metrics data into chart data.
110        This is trivial for single values like the throttle, the cell voltage chart is an example
111        on basic array processing.</p>
112
113      <p>Also, while charts <em>can</em> be defined with few options, you'll <em>love</em> to explore
114        all the features and fine tuning options provided by Highcharts. For inspiration,
115        have a look at the <a target="_blank" href="https://www.highcharts.com/demo">Highcharts demos</a>
116        and the <a target="_blank" href="https://www.highcharts.com/docs/">Highcharts documentation</a>.
117        We're using <a target="_blank" href="https://www.highcharts.com/docs/chart-design-and-style/style-by-css">
118        styled mode</a>, so some options don't apply, but everything can be styled by standard CSS.</p>
119
120    </div>
121
122  </div>
123</div>
124
125<script>
126(function(){
127
128  /* Get page source before chart rendering: */
129  var pagesrc = $('#main').html();
130
131  /* Init throttle gauge: */
132  $("#throttle-gauge").chart({
133    chart: {
134      type: 'gauge',
135      spacing: [0, 0, 0, 0],
136      margin: [0, 0, 0, 0],
137      animation: { duration: 500, easing: 'easeOutExpo' },
138    },
139    title: { text: "Throttle", verticalAlign: "middle", y: 75 },
140    credits: { enabled: false },
141    tooltip: { enabled: false },
142    plotOptions: {
143      gauge: { dataLabels: { enabled: false }, overshoot: 1 }
144    },
145    pane: [{
146      startAngle: -125, endAngle: 125, size: '100%', center: ['50%', '60%']
147    }],
148    yAxis: [{
149      title: { text: '%' },
150      className: 'throttle',
151      reversed: false,
152      min: 0, max: 100,
153      plotBands: [
154        { from: 0, to: 60, className: 'green-band' },
155        { from: 60, to: 80, className: 'yellow-band' },
156        { from: 80, to: 100, className: 'red-band' },
157      ],
158      minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',
159      tickPixelInterval: 40, tickPosition: 'inside', tickLength: 13,
160      labels: { step: 2, distance: -28, x: 0, y: 5, zIndex: 2 },
161    }],
162    series: [{
163      name: 'Throttle', data: [0],
164      className: 'throttle',
165      animation: { duration: 0 },
166      pivot: { radius: '10' },
167      dial: { radius: '88%', topWidth: 1, baseLength: '20%', baseWidth: 10, rearLength: '20%' },
168    }],
169    /* Update method: */
170    onUpdate: function(update) {
171      // Create gauge data set from metric:
172      var data = [ metrics["v.e.throttle"] ];
173      // Update chart:
174      this.series[0].setData(data);
175    },
176  });
177
178  /* Init cell voltages chart */
179  $("#cell-voltages").chart({
180    chart: {
181      type: 'column',
182      animation: { duration: 500, easing: 'easeOutExpo' },
183    },
184    title: { text: "Cell Voltages" },
185    credits: { enabled: false },
186    tooltip: {
187      enabled: true,
188      shared: true,
189      headerFormat: 'Cell #{point.key}:<br/>',
190      pointFormat: '{series.name}: <b>{point.y}</b><br/>',
191      valueSuffix: " V"
192    },
193    legend: { enabled: true },
194    xAxis: {
195      categories: []
196    },
197    yAxis: [{
198      title: { text: null },
199      labels: { format: "{value:.2f}V" },
200      tickAmount: 4, startOnTick: false, endOnTick: false,
201      floor: 3.3, ceiling: 4.2,
202      minorTickInterval: 'auto',
203    }],
204    series: [{
205      name: 'Current', data: [],
206      className: 'cell-voltage',
207      animation: { duration: 0 },
208    },{
209      name: 'Minimum', data: [],
210      className: 'cell-voltage-min',
211      animation: { duration: 0 },
212    }],
213    /* Update method: */
214    onUpdate: function(update) {
215      // Note: the 'update' parameter contains the actual update set.
216      // You can use this to reduce chart updates to the actual changes.
217      // For this demo, we just use the global metrics object:
218      var
219        m_vlt = metrics["v.b.c.voltage"] || [],
220        m_min = metrics["v.b.c.voltage.min"] || [];
221      // Create categories (cell numbers) & rounded values:
222      var cat = [], val0 = [], val1 = [];
223      for (var i = 0; i < m_vlt.length; i++) {
224        cat.push(i+1);
225        val0.push(Number((m_vlt[i]||0).toFixed(3)));
226        val1.push(Number((m_min[i]||0).toFixed(3)));
227      }
228      // Update chart:
229      this.xAxis[0].setCategories(cat);
230      this.series[0].setData(val0);
231      this.series[1].setData(val1);
232    },
233  });
234
235  /* Test metrics generator: */
236  $('.action-gendata').on('click', function() {
237    var td = {};
238    td["m.net.provider"] = ["hologram","Vodafone","Telekom"][Math.floor(Math.random()*3)];
239    td["v.e.throttle"] = Math.random() * 100;
240    td["v.b.12v.voltage.ref"] = 10 + Math.random() * 4;
241    var m_vlt = [], m_min = [];
242    for (var i = 1; i <= 16; i++) {
243      m_vlt.push(3.6 + Math.random() * 0.5);
244      m_min.push(3.4 + Math.random() * 0.2);
245    }
246    td["v.b.c.voltage"] = m_vlt;
247    td["v.b.c.voltage.min"] = m_min;
248    $('.receiver').trigger('msg:metrics', $.extend(metrics, td));
249  });
250
251  /* Display page source: */
252  $('.action-showsrc').on('click', function() {
253    $('<div/>').dialog({
254      title: 'Source Code',
255      body: '<pre style="font-size:85%; height:calc(100vh - 230px);">'
256        + encode_html(pagesrc) + '</pre>',
257      size: 'lg',
258    });
259  });
260
261})();
262</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>