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