86 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/www/nobs.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | }
6 |
7 | body {
8 | margin: 0;
9 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
10 | font-size: 1rem;
11 | font-weight: 400;
12 | line-height: 1.5;
13 | color: #212529;
14 | background-color: #fff;
15 | -webkit-text-size-adjust: 100%;
16 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
17 | }
18 |
19 | input,
20 | button,
21 | select,
22 | optgroup,
23 | textarea {
24 | margin: 0;
25 | font-family: inherit;
26 | font-size: inherit;
27 | line-height: inherit;
28 | }
29 |
30 | button,
31 | input[type='submit'],
32 | input[type='button'],
33 | input[type='checkbox'] {
34 | cursor: pointer;
35 | }
36 |
37 | input:disabled,
38 | select:disabled,
39 | button:disabled,
40 | textarea:disabled {
41 | cursor: not-allowed;
42 | opacity: .5;
43 | }
44 |
45 | .container {
46 | width: 100%;
47 | padding-right: calc(1.5rem * 0.5);
48 | padding-left: calc(1.5rem * 0.5);
49 | margin-right: auto;
50 | margin-left: auto;
51 | }
52 |
53 |
54 | hr {
55 | /*margin: 1rem 0;*/
56 | /*color: inherit;*/
57 | border: 0;
58 | border-top: 1px solid;
59 | opacity: 0.25;
60 | }
61 |
62 | /* === Button === */
63 |
64 | .btn {
65 | --bs-btn-padding-x: 0.75rem;
66 | --bs-btn-padding-y: 0.375rem;
67 | --bs-btn-font-size: 1rem;
68 | --bs-btn-font-weight: 400;
69 | --bs-btn-line-height: 1.5;
70 | --bs-btn-border-width: 1px;
71 | --bs-btn-border-radius: 0.25rem;
72 | display: inline-block;
73 | padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
74 | font-family: var(--bs-btn-font-family);
75 | font-size: var(--bs-btn-font-size);
76 | font-weight: var(--bs-btn-font-weight);
77 | line-height: var(--bs-btn-line-height);
78 | color: var(--bs-btn-color);
79 | text-align: center;
80 | text-decoration: none;
81 | vertical-align: middle;
82 | cursor: pointer;
83 | user-select: none;
84 | border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
85 | border-radius: var(--bs-btn-border-radius);
86 | background-color: var(--bs-btn-bg);
87 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
88 | }
89 |
90 | .btn-primary {
91 | --bs-btn-color: #fff;
92 | --bs-btn-bg: #0d6efd;
93 | --bs-btn-border-color: #0d6efd;
94 | --bs-btn-hover-color: #fff;
95 | --bs-btn-hover-bg: #0b5ed7;
96 | --bs-btn-hover-border-color: #0a58ca;
97 | --bs-btn-focus-shadow-rgb: 49, 132, 253;
98 | }
99 |
100 | .btn-secondary {
101 | --bs-btn-color: #fff;
102 | --bs-btn-bg: #6c757d;
103 | --bs-btn-border-color: #6c757d;
104 | --bs-btn-hover-color: #fff;
105 | --bs-btn-hover-bg: #5c636a;
106 | --bs-btn-hover-border-color: #565e64;
107 | --bs-btn-focus-shadow-rgb: 130, 138, 145;
108 | }
109 |
110 | .btn-danger {
111 | --bs-btn-color: #fff;
112 | --bs-btn-bg: #dc3545;
113 | --bs-btn-border-color: #dc3545;
114 | --bs-btn-hover-color: #fff;
115 | --bs-btn-hover-bg: #bb2d3b;
116 | --bs-btn-hover-border-color: #b02a37;
117 | --bs-btn-focus-shadow-rgb: 225, 83, 97;
118 | }
119 |
120 | .btn-outline-primary {
121 | --bs-btn-color: #0d6efd;
122 | --bs-btn-border-color: #0d6efd;
123 | --bs-btn-hover-color: #fff;
124 | --bs-btn-hover-bg: #0d6efd;
125 | --bs-btn-hover-border-color: #0d6efd;
126 | --bs-btn-focus-shadow-rgb: 13, 110, 253;
127 | }
128 |
129 | .btn-outline-secondary {
130 | --bs-btn-color: #6c757d;
131 | --bs-btn-border-color: #6c757d;
132 | --bs-btn-hover-color: #fff;
133 | --bs-btn-hover-bg: #6c757d;
134 | --bs-btn-hover-border-color: #6c757d;
135 | --bs-btn-focus-shadow-rgb: 108, 117, 125;
136 | }
137 |
138 | .btn-outline-danger {
139 | --bs-btn-color: #dc3545;
140 | --bs-btn-border-color: #dc3545;
141 | --bs-btn-hover-color: #fff;
142 | --bs-btn-hover-bg: #dc3545;
143 | --bs-btn-hover-border-color: #dc3545;
144 | --bs-btn-focus-shadow-rgb: 220, 53, 69;
145 | }
146 |
147 | .btn:hover:not([disabled]) {
148 | color: var(--bs-btn-hover-color);
149 | background-color: var(--bs-btn-hover-bg);
150 | border-color: var(--bs-btn-hover-border-color);
151 | }
152 |
153 |
154 | /* === Buttongroup === */
155 |
156 | .btn-group {
157 | border-radius: 0.375rem;
158 | /*position: relative;*/
159 | display: inline-flex;
160 | }
161 |
162 | .btn-group > .btn:not(:first-child),
163 | .btn-group > .btn-group:not(:first-child) {
164 | margin-left: -1px;
165 | }
166 |
167 | .btn-group > .btn:not(:last-child):not(.dropdown-toggle),
168 | .btn-group > .btn.dropdown-toggle-split:first-child,
169 | .btn-group > .btn-group:not(:last-child) > .btn {
170 | border-top-right-radius: 0;
171 | border-bottom-right-radius: 0;
172 | }
173 |
174 | .btn-group > .btn:nth-child(n+3),
175 | .btn-group > :not(.btn-check) + .btn,
176 | .btn-group > .btn-group:not(:first-child) > .btn {
177 | border-top-left-radius: 0;
178 | border-bottom-left-radius: 0;
179 | }
180 |
181 | /* === Select === */
182 |
183 | .form-select {
184 | display: block;
185 | width: 100%;
186 | padding: 0.375rem 2.25rem 0.375rem 0.75rem;
187 | -moz-padding-start: calc(0.75rem - 3px);
188 | font-size: 1rem;
189 | font-weight: 400;
190 | line-height: 1.5;
191 | color: #212529;
192 | background-color: #fff;
193 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
194 | background-repeat: no-repeat;
195 | background-position: right 0.75rem center;
196 | background-size: 16px 12px;
197 | border: 1px solid #ced4da;
198 | border-radius: 0.375rem;
199 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
200 | -webkit-appearance: none;
201 | -moz-appearance: none;
202 | appearance: none;
203 | }
204 |
205 | .form-select:focus {
206 | border-color: #86b7fe;
207 | outline: 0;
208 | box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
209 | }
210 |
211 | .form-select-sm {
212 | padding-top: 0.25rem;
213 | padding-bottom: 0.25rem;
214 | padding-left: 0.5rem;
215 | font-size: 0.875rem;
216 | border-radius: 0.25rem;
217 | }
218 |
219 | .form-select-lg {
220 | padding-top: 0.5rem;
221 | padding-bottom: 0.5rem;
222 | padding-left: 1rem;
223 | font-size: 1.25rem;
224 | border-radius: 0.5rem;
225 | }
226 |
227 |
228 | /* === Range === */
229 |
230 | .form-range {
231 | width: 100%;
232 | height: 1.5rem;
233 | padding: 0;
234 | background-color: transparent;
235 | -webkit-appearance: none;
236 | -moz-appearance: none;
237 | appearance: none;
238 | }
239 |
240 | .form-range:focus {
241 | outline: 0;
242 | }
243 |
244 | .form-range:focus::-webkit-slider-thumb {
245 | box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
246 | }
247 | .form-range:focus::-moz-range-thumb {
248 | box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
249 | }
250 | .form-range::-moz-focus-outer {
251 | border: 0;
252 | }
253 |
254 | .form-range::-webkit-slider-thumb {
255 | width: 1rem;
256 | height: 1rem;
257 | margin-top: -0.25rem;
258 | background-color: #0d6efd;
259 | border: 0;
260 | border-radius: 1rem;
261 | -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
262 | transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
263 | -webkit-appearance: none;
264 | appearance: none;
265 | }
266 |
267 | @media (prefers-reduced-motion: reduce) {
268 | .form-range::-webkit-slider-thumb {
269 | -webkit-transition: none;
270 | transition: none;
271 | }
272 | }
273 |
274 | .form-range::-webkit-slider-thumb:active {
275 | background-color: #b6d4fe;
276 | }
277 | .form-range::-webkit-slider-runnable-track {
278 | width: 100%;
279 | height: 0.5rem;
280 | color: transparent;
281 | cursor: pointer;
282 | background-color: #dee2e6;
283 | border-color: transparent;
284 | border-radius: 1rem;
285 | }
286 |
287 | .form-range::-moz-range-thumb {
288 | width: 1rem;
289 | height: 1rem;
290 | background-color: #0d6efd;
291 | border: 0;
292 | border-radius: 1rem;
293 | -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
294 | transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
295 | -moz-appearance: none;
296 | appearance: none;
297 | }
298 |
299 | .form-range::-moz-range-thumb:active {
300 | background-color: #b6d4fe;
301 | }
302 |
303 | .form-range::-moz-range-track {
304 | width: 100%;
305 | height: 0.5rem;
306 | color: transparent;
307 | cursor: pointer;
308 | background-color: #dee2e6;
309 | border-color: transparent;
310 | border-radius: 1rem;
311 | }
--------------------------------------------------------------------------------
/www/pflow.js:
--------------------------------------------------------------------------------
1 | /**
2 |
3 | HTML/Javascript view for energy flow in building automation
4 |
5 | Setup example:
6 | var config = {
7 | table: [
8 | {id: 'pv', type: 'pv'},
9 | {id: 'home', type: 'home', sign: -1},
10 | {id: 'bat', type: 'bat', sign: -1},
11 | {id: 'grid', type: 'grid'},
12 | {id: 'car', type: 'car', sign: -1, wallbox: true}],
13 | bar: {
14 | in: [
15 | {id: 'pv', type: 'pv'},
16 | {id: 'bat', type: 'bat', sign: -1},
17 | {id: 'grid', type: 'grid'}],
18 | out: [
19 | {id: 'home', type: 'home'},
20 | {id: 'car', type: 'car'},
21 | {id: 'bat', type: 'bat'},
22 | {id: 'grid', type: 'grid', sign: -1}]
23 | }
24 | }
25 | pflow = new Pflow('pflow-table', 'pflow-bar', setup);
26 |
27 |
28 | Data example:
29 | var d = {
30 | pv: {
31 | power: 2533,
32 | subline: 'Süd: 600 W Nord: 400 W'},
33 | home: {
34 | power: 411 },
35 | bat: {
36 | power: 0,
37 | info: '90%',
38 | subline: 'Automatik - Schlafen'},
39 | car: {
40 | power: 0,
41 | info: '11.2 kWh',
42 | subline: 'PV - Laden gesperrt',
43 | disable: false,
44 | wallbox_pvready: false,
45 | wallbox_stop: true,
46 | wallbox_amp: '1x6A'},
47 | grid: {
48 | power: -2122
49 | },
50 | };
51 |
52 | pflow.update(d);
53 |
54 | Config details:
55 |
56 | table
57 | id identifier for access
58 | type set to use default color and icon [pv, home, bat, grid, car heat]
59 | sign -1 to invert the direction for the arrow
60 | icon manual icon setting, e.g.: 'svg-house'
61 | wallbox true to activate wallbox options
62 |
63 | bar.*
64 | id identifier for access
65 | type set to use default color and icon [pv, home, bat, grid, car heat]
66 | sign -1 for negative power input to be positive
67 | icon manual icon setting, e.g.: 'svg-house'
68 |
69 | Data details:
70 |
71 | power: Power in watt
72 | info: Text behind power
73 | subline: Infotext below power and info
74 | disable: Icon is shown disabled
75 | error: Icon is shown as error
76 |
77 | wallbox_pvready: sun or cloud icon
78 | wallbox_stop: badge with phase and ampere is shown diabled
79 | wallbox_amp: Badge for phase and ampere info '3x16A'
80 |
81 |
82 | 07.11.2022 Martin Steppuhn
83 | 26.11.2022 Martin Steppuhn error for icon
84 |
85 |
86 | */
87 |
88 | class Pflow {
89 | /**
90 | * Constructor/Init
91 | *
92 | * @param table_id Element ID for the table view
93 | * @param bar_id Element ID for the bars view
94 | * @param config Configuration
95 | */
96 | constructor(table_id, bar_id, config) {
97 | this.table_id = table_id; // container for table
98 | this.bar_id = bar_id; // container for in and out bar
99 | this.config = config; // complete configuration
100 | this.arrow_power_scale = 2000; // power in watt for max arrow size
101 |
102 | // if not specified in config, the icon ist dedicated by the type pv --> svg-sun
103 | this.default_icons = {
104 | pv: 'svg-sun',
105 | home: 'svg-house',
106 | bat: 'svg-car-battery',
107 | grid: 'svg-industry',
108 | car: 'svg-car',
109 | heat: 'svg-fire'
110 | };
111 |
112 | // Init
113 | this.init_resource(); // append svgs and styles
114 | this.init_table();
115 | this.init_bar('in');
116 | this.init_bar('out');
117 | this.update();
118 | }
119 |
120 | /**
121 | * Init table view
122 | */
123 | init_table() {
124 | var element = document.getElementById(this.table_id);
125 |
126 | if (!element || !(this.config?.table ?? null)) { // abort without container or config
127 | this.config.table = null;
128 | return;
129 | }
130 |
131 | var s = '';
132 | var cfg = this.config.table;
133 | for (var i = 0; i < cfg.length; i++) {
134 | var row = cfg[i];
135 |
136 | if (row.wallbox) {
137 | var wallbox = `
138 |
139 |
`;
140 | } else wallbox = "";
141 |
142 | var svg_icon = row?.icon ?? this.default_icons[row.type]; // get custom or default icon
143 |
144 | // "Template" for a single table row
145 | s += `
146 |
147 |
152 |
153 |
154 |
155 | ${wallbox}
156 |
157 |
158 |
159 |
160 |
170 |
`;
171 | }
172 | element.innerHTML = s;
173 | }
174 |
175 | /**
176 | * Init bar
177 | */
178 | init_bar(bar_name) {
179 | var element = document.getElementById(this.bar_id)
180 | var cfg = this.config?.bar?.[bar_name] ?? null;
181 |
182 | if (element && cfg) {
183 | var s = `
`;
184 | for (var i = 0; i < cfg.length; i++) {
185 | var bar = cfg[i];
186 | var svg_icon = bar?.icon ?? this.default_icons[bar.type];
187 | s += `
188 |
189 |
`;
190 | }
191 | element.innerHTML += s;
192 | } else {
193 | this.config.bar = null; // disable by removing the config
194 | }
195 |
196 | }
197 |
198 | /**
199 | * Update table and bars
200 | */
201 | update(data) {
202 | this.update_table(data);
203 | this.update_bar('in', data);
204 | this.update_bar('out', data);
205 | }
206 |
207 | /**
208 | * Update table with given data
209 | *
210 | * For each entry the following values could be set: [disable, power, info, subline]
211 | * and anditional with wallbox option: [wallbox_pvready, wallbox_stop, wallbox_amp]
212 | *
213 | * @param data
214 | */
215 | update_table(data) {
216 | if (!this.config.table) return;
217 |
218 | data = data || {};
219 |
220 | for (var row of this.config.table) {
221 | var d = data?.[row.id] ?? {};
222 | var id = row.id;
223 | var row_element = document.querySelector(`#${this.table_id} [data-row="${id}"]`);
224 | var p = (data?.[id]?.power ?? 0) * (row?.sign ?? 1);
225 |
226 | this.update_table_arrow(row_element.querySelector('[data-id="arrow"]'), p);
227 |
228 | // === Icon ===
229 | row_element.querySelector('[data-id="icon"]').classList.toggle('pft-fill-error', (d?.error ?? false));
230 | row_element.querySelector('[data-id="icon"]').classList.toggle('pft-fill-disable', (d?.disable ?? false));
231 | row_element.querySelector('[data-id="icon"]').classList.toggle('pft-fill-enable', !(d?.disable ?? false) && !(d?.error ?? false));
232 |
233 | // === Text ===
234 | row_element.querySelector('[data-id="power"]').textContent = (isNaN(d.power)) ? '--- W' : Math.abs(d.power) + ' W';
235 | row_element.querySelector('[data-id="info"]').textContent = d?.info ?? '';
236 | row_element.querySelector('[data-id="subline"]').textContent = d?.subline ?? '';
237 |
238 | // === Wallbox ===
239 | if (d?.wallbox_pvready === true) {
240 | row_element.querySelector('[data-id="wallbox-sun"]').setAttribute('visibility', 'visible');
241 | row_element.querySelector('[data-id="wallbox-cloud"]').setAttribute('visibility', 'hidden');
242 | } else if (d?.wallbox_pvready === false) {
243 | row_element.querySelector('[data-id="wallbox-sun"]').setAttribute('visibility', 'hidden');
244 | row_element.querySelector('[data-id="wallbox-cloud"]').setAttribute('visibility', 'visible');
245 | }
246 |
247 | var ele = row_element.querySelector('[data-id="wallbox-amp"]');
248 | if (ele) {
249 | ele.classList.toggle('pft-wallbox-off', (d?.wallbox_stop === true));
250 | ele.classList.toggle('pft-wallbox-on', (d?.wallbox_stop === false));
251 | ele.textContent = d?.wallbox_amp ?? '';
252 | }
253 | }
254 | }
255 |
256 | /**
257 | * Update single arrow for a table row.
258 | *
259 | * @param element HTML Element
260 | * @param value Power value in watt
261 | */
262 | update_table_arrow(element, value) {
263 | value = (value / this.arrow_power_scale) * 100; // power for 100%
264 |
265 | var sign = (value > 0) ? -1 : 1;
266 | if (value > 100) value = 100;
267 | if (value < -100) value = -100;
268 | var line_width = this.scale_value(Math.abs(value), 5, 20);
269 |
270 | // console.log("arrow_update", sid, value, invert);
271 |
272 | if (value) {
273 | var d = this.scale_value(Math.abs(value), 10, 50 - line_width) * sign;
274 | element.setAttribute('d', `M ${-d / 2} ${-d} L ${d / 2} 0 L ${-d / 2} ${+d}`);
275 | element.setAttribute('stroke-width', line_width);
276 | } else {
277 | element.setAttribute('d', '');
278 | }
279 | }
280 |
281 | /**
282 | * Scale a 0..100% variable between two borders
283 | *
284 | * @param value
285 | * @param min
286 | * @param max
287 | * @returns value [min..max]
288 | */
289 | scale_value(value, min, max) {
290 | value = ((max - min) / 100) * value + min;
291 | if (value > max) value = max;
292 | return value;
293 | }
294 |
295 | /**
296 | * Update bar
297 | *
298 | * @param bar_name in/out
299 | * @data data
300 | */
301 | update_bar(bar_name, data) {
302 | var n, i, p;
303 | var bar;
304 | var cfg = this.config?.bar?.[bar_name] ?? null;
305 |
306 | if (!cfg) return;
307 |
308 | var sum = 0;
309 |
310 | for (i = 0; i < cfg.length; i++) {
311 | bar = cfg[i];
312 | p = parseInt(data?.[bar.id]?.power ?? 0) * (bar?.sign ?? 1);
313 | sum += Math.max(0, p);
314 | }
315 |
316 | var percentage;
317 | var percentage_sum = 0;
318 |
319 | var element = document.querySelector(`#${this.bar_id} [data-bar=${bar_name}]`);
320 |
321 | // console.log("!!!", element);
322 |
323 | for (i = 0; i < cfg.length; i++) {
324 | bar = cfg[i];
325 | p = parseInt(data?.[bar.id]?.power ?? 0) * (bar?.sign ?? 1);
326 | p = Math.max(0, p);
327 |
328 | if (i < cfg.length - 1) {
329 | percentage = Math.round((p / sum) * 100);
330 | percentage_sum += percentage;
331 | } else
332 | percentage = 100 - percentage_sum;
333 |
334 | // console.log("update_bar", n, i, sum, bar, p, percentage);
335 | element.querySelector('[data-id="' + bar.id + '"]').style.width = percentage + '%';
336 | }
337 | }
338 |
339 | /**
340 | * Init resources
341 | */
342 | init_resource() {
343 | if (document.getElementById('pflow-resource')) return; // resource already defined
344 | var resource = `
345 |
375 | `;
495 | document.head.innerHTML = resource + document.head.innerHTML;
496 | }
497 | }
--------------------------------------------------------------------------------
/www/trace-charge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
CHARGE
9 |
10 |
11 |
12 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/www/trace-feed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
FEED
9 |
10 |
11 |
12 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/www/utils.js:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | /**
6 | * Check if a variable is a number.
7 | *
8 | * @param value
9 | * @returns {boolean}
10 | */
11 | function is_number(value) {
12 | return ((typeof (value) === 'number'));
13 | }
14 |
15 |
16 | /**
17 | * Fetch JSON (AJAX Request)
18 | *
19 | * @param url requested url
20 | * @param success callback for success
21 | * @param error callback for error (optional)
22 | * @param post dictionary to send as json post (optional)
23 | */
24 | function fetch_json(url, success, error, post) {
25 |
26 | if (post) {
27 | post = {
28 | method: 'POST',
29 | body: JSON.stringify(post),
30 | headers: {'Content-Type': 'application/json'}
31 | }
32 | }
33 |
34 | fetch(url, post).then(function (response) {
35 | if (response.ok) {
36 | return response.json();
37 | } else {
38 | return Promise.reject(response);
39 | }
40 | }).then(function (data) {
41 | success(data);
42 | }).catch(function (err) {
43 | if (error) error(err);
44 | });
45 | }
46 |
47 |
48 | /**
49 | * Format float or integer with --- as default.
50 | *
51 | * Example usage: document.getElementById('value1').textContent = fmtNumber(data.x.y ?? null, 2) + ' A';
52 | * @param value float or integer
53 | * @param precision number of digits displayed after the decimal point
54 | * @returns {string}
55 | */
56 | fmtNumber = function (value, precision) {
57 | try {
58 | if (isNaN(value)) throw true;
59 | return value.toFixed(precision);
60 | } catch (e) {
61 | return (precision) ? '-.' + '-'.repeat(precision) : "---";
62 | }
63 | };
64 |
--------------------------------------------------------------------------------