]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/panel/RRDChart.js
bump version to 4.2.3
[proxmox-widget-toolkit.git] / src / panel / RRDChart.js
1 // override the download server url globally, for privacy reasons
2 Ext.draw.Container.prototype.defaultDownloadServerUrl = "-";
3
4 Ext.define('Proxmox.chart.axis.segmenter.NumericBase2', {
5 extend: 'Ext.chart.axis.segmenter.Numeric',
6 alias: 'segmenter.numericBase2',
7
8 // derived from the original numeric segmenter but using 2 instead of 10 as base
9 preferredStep: function(min, estStepSize) {
10 // Getting an order of magnitude of the estStepSize with a common logarithm.
11 let order = Math.floor(Math.log2(estStepSize));
12 let scale = Math.pow(2, order);
13
14 estStepSize /= scale;
15
16 // FIXME: below is not useful when using base 2 instead of base 10, we could
17 // just directly set estStepSize to 2
18 if (estStepSize <= 1) {
19 estStepSize = 1;
20 } else if (estStepSize < 2) {
21 estStepSize = 2;
22 }
23 return {
24 unit: {
25 // When passed estStepSize is less than 1, its order of magnitude
26 // is equal to -number_of_leading_zeros in the estStepSize.
27 fixes: -order, // Number of fractional digits.
28 scale: scale,
29 },
30 step: estStepSize,
31 };
32 },
33
34 /**
35 * Wraps the provided estimated step size of a range without altering it into a step size object.
36 *
37 * @param {*} min The start point of range.
38 * @param {*} estStepSize The estimated step size.
39 * @return {Object} Return the step size by an object of step x unit.
40 * @return {Number} return.step The step count of units.
41 * @return {Object} return.unit The unit.
42 */
43 // derived from the original numeric segmenter but using 2 instead of 10 as base
44 exactStep: function(min, estStepSize) {
45 let order = Math.floor(Math.log2(estStepSize));
46 let scale = Math.pow(2, order);
47
48 return {
49 unit: {
50 // add one decimal point if estStepSize is not a multiple of scale
51 fixes: -order + (estStepSize % scale === 0 ? 0 : 1),
52 scale: 1,
53 },
54 step: estStepSize,
55 };
56 },
57 });
58
59 Ext.define('Proxmox.widget.RRDChart', {
60 extend: 'Ext.chart.CartesianChart',
61 alias: 'widget.proxmoxRRDChart',
62
63 unit: undefined, // bytes, bytespersecond, percent
64
65 powerOfTwo: false,
66
67 // set to empty string to suppress warning in debug mode
68 downloadServerUrl: '-',
69
70 controller: {
71 xclass: 'Ext.app.ViewController',
72
73 init: function(view) {
74 this.powerOfTwo = view.powerOfTwo;
75 },
76
77 convertToUnits: function(value) {
78 let units = ['', 'k', 'M', 'G', 'T', 'P'];
79 let si = 0;
80 let format = '0.##';
81 if (value < 0.1) format += '#';
82 const baseValue = this.powerOfTwo ? 1024 : 1000;
83 while (value >= baseValue && si < units.length -1) {
84 value = value / baseValue;
85 si++;
86 }
87
88 // javascript floating point weirdness
89 value = Ext.Number.correctFloat(value);
90
91 // limit decimal points
92 value = Ext.util.Format.number(value, format);
93
94 let unit = units[si];
95 if (unit && this.powerOfTwo) unit += 'i';
96
97 return `${value.toString()} ${unit}`;
98 },
99
100 leftAxisRenderer: function(axis, label, layoutContext) {
101 let me = this;
102 return me.convertToUnits(label);
103 },
104
105 onSeriesTooltipRender: function(tooltip, record, item) {
106 let view = this.getView();
107
108 let suffix = '';
109 if (view.unit === 'percent') {
110 suffix = '%';
111 } else if (view.unit === 'bytes') {
112 suffix = 'B';
113 } else if (view.unit === 'bytespersecond') {
114 suffix = 'B/s';
115 }
116
117 let prefix = item.field;
118 if (view.fieldTitles && view.fieldTitles[view.fields.indexOf(item.field)]) {
119 prefix = view.fieldTitles[view.fields.indexOf(item.field)];
120 }
121 let v = this.convertToUnits(record.get(item.field));
122 let t = new Date(record.get('time'));
123 tooltip.setHtml(`${prefix}: ${v}${suffix}<br>${t}`);
124 },
125
126 onAfterAnimation: function(chart, eopts) {
127 if (!chart.header || !chart.header.tools) {
128 return;
129 }
130 // if the undo button is disabled, disable our tool
131 let ourUndoZoomButton = chart.header.tools[0];
132 let undoButton = chart.interactions[0].getUndoButton();
133 ourUndoZoomButton.setDisabled(undoButton.isDisabled());
134 },
135 },
136
137 width: 770,
138 height: 300,
139 animation: false,
140 interactions: [
141 {
142 type: 'crosszoom',
143 },
144 ],
145 legend: {
146 type: 'dom',
147 padding: 0,
148 },
149 listeners: {
150 redraw: {
151 fn: 'onAfterAnimation',
152 options: {
153 buffer: 500,
154 },
155 },
156 },
157
158 touchAction: {
159 panX: true,
160 panY: true,
161 },
162
163 constructor: function(config) {
164 let me = this;
165
166 let segmenter = config.powerOfTwo ? 'numericBase2' : 'numeric';
167 config.axes = [
168 {
169 type: 'numeric',
170 position: 'left',
171 grid: true,
172 renderer: 'leftAxisRenderer',
173 minimum: 0,
174 segmenter,
175 },
176 {
177 type: 'time',
178 position: 'bottom',
179 grid: true,
180 fields: ['time'],
181 },
182 ];
183 me.callParent([config]);
184 },
185
186 checkThemeColors: function() {
187 let me = this;
188 let rootStyle = getComputedStyle(document.documentElement);
189
190 // get colors
191 let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
192 let text = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
193 let primary = rootStyle.getPropertyValue("--pwt-chart-primary").trim() || "#000000";
194 let gridStroke = rootStyle.getPropertyValue("--pwt-chart-grid-stroke").trim() || "#dddddd";
195
196 // set the colors
197 me.setBackground(background);
198 me.axes.forEach((axis) => {
199 axis.setLabel({ color: text });
200 axis.setTitle({ color: text });
201 axis.setStyle({ strokeStyle: primary });
202 axis.setGrid({ stroke: gridStroke });
203 });
204 me.redraw();
205 },
206
207 initComponent: function() {
208 let me = this;
209
210 if (!me.store) {
211 throw "cannot work without store";
212 }
213
214 if (!me.fields) {
215 throw "cannot work without fields";
216 }
217
218 me.callParent();
219
220 // add correct label for left axis
221 let axisTitle = "";
222 if (me.unit === 'percent') {
223 axisTitle = "%";
224 } else if (me.unit === 'bytes') {
225 axisTitle = "Bytes";
226 } else if (me.unit === 'bytespersecond') {
227 axisTitle = "Bytes/s";
228 } else if (me.fieldTitles && me.fieldTitles.length === 1) {
229 axisTitle = me.fieldTitles[0];
230 } else if (me.fields.length === 1) {
231 axisTitle = me.fields[0];
232 }
233
234 me.axes[0].setTitle(axisTitle);
235
236 me.updateHeader();
237
238 if (me.header && me.legend) {
239 me.header.padding = '4 9 4';
240 me.header.add(me.legend);
241 me.legend = undefined;
242 }
243
244 if (!me.noTool) {
245 me.addTool({
246 type: 'minus',
247 disabled: true,
248 tooltip: gettext('Undo Zoom'),
249 handler: function() {
250 let undoButton = me.interactions[0].getUndoButton();
251 if (undoButton.handler) {
252 undoButton.handler();
253 }
254 },
255 });
256 }
257
258 // add a series for each field we get
259 me.fields.forEach(function(item, index) {
260 let title = item;
261 if (me.fieldTitles && me.fieldTitles[index]) {
262 title = me.fieldTitles[index];
263 }
264 me.addSeries(Ext.apply(
265 {
266 type: 'line',
267 xField: 'time',
268 yField: item,
269 title: title,
270 fill: true,
271 style: {
272 lineWidth: 1.5,
273 opacity: 0.60,
274 },
275 marker: {
276 opacity: 0,
277 scaling: 0.01,
278 },
279 highlightCfg: {
280 opacity: 1,
281 scaling: 1.5,
282 },
283 tooltip: {
284 trackMouse: true,
285 renderer: 'onSeriesTooltipRender',
286 },
287 },
288 me.seriesConfig,
289 ));
290 });
291
292 // enable animation after the store is loaded
293 me.store.onAfter('load', function() {
294 me.setAnimation({
295 duration: 200,
296 easing: 'easeIn',
297 });
298 }, this, { single: true });
299
300
301 me.checkThemeColors();
302
303 // switch colors on media query changes
304 me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
305 me.themeListener = (e) => { me.checkThemeColors(); };
306 me.mediaQueryList.addEventListener("change", me.themeListener);
307 },
308
309 doDestroy: function() {
310 let me = this;
311
312 me.mediaQueryList.removeEventListener("change", me.themeListener);
313
314 me.callParent();
315 },
316 });