Total Complexity | 72 |
Total Lines | 323 |
Duplicated Lines | 0 % |
Complex classes like Orange.widgets.utils.plot.OWAxis often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """ |
||
47 | class OWAxis(QGraphicsItem): |
||
48 | Role = OWPalette.Axis |
||
49 | |||
50 | def __init__(self, id, title='', title_above=False, title_location=AxisMiddle, |
||
51 | line=None, arrows=0, plot=None, bounds=None): |
||
52 | QGraphicsItem.__init__(self) |
||
53 | self.setFlag(QGraphicsItem.ItemHasNoContents) |
||
54 | self.setZValue(AxisZValue) |
||
55 | self.id = id |
||
56 | self.title = title |
||
57 | self.title_location = title_location |
||
58 | self.data_line = line |
||
59 | self.plot = plot |
||
60 | self.graph_line = None |
||
61 | self.size = None |
||
62 | self.scale = None |
||
63 | self.tick_length = (10, 5, 0) |
||
64 | self.arrows = arrows |
||
65 | self.title_above = title_above |
||
66 | self.line_item = QGraphicsLineItem(self) |
||
67 | self.title_item = QGraphicsTextItem(self) |
||
68 | self.end_arrow_item = None |
||
69 | self.start_arrow_item = None |
||
70 | self.show_title = False |
||
71 | self.scale = None |
||
72 | path = QPainterPath() |
||
73 | path.setFillRule(Qt.WindingFill) |
||
74 | path.moveTo(0, 3.09) |
||
75 | path.lineTo(0, -3.09) |
||
76 | path.lineTo(9.51, 0) |
||
77 | path.closeSubpath() |
||
78 | self.arrow_path = path |
||
79 | self.label_items = [] |
||
80 | self.label_bg_items = [] |
||
81 | self.tick_items = [] |
||
82 | self._ticks = [] |
||
83 | self.zoom_transform = QTransform() |
||
84 | self.labels = None |
||
85 | self.values = None |
||
86 | self._bounds = bounds |
||
87 | self.auto_range = None |
||
88 | self.auto_scale = True |
||
89 | |||
90 | self.zoomable = False |
||
91 | self.update_callback = None |
||
92 | self.max_text_width = 50 |
||
93 | self.text_margin = 5 |
||
94 | self.always_horizontal_text = False |
||
95 | |||
96 | @staticmethod |
||
97 | def compute_scale(min, max): |
||
98 | magnitude = int(3 * log10(abs(max - min)) + 1) |
||
99 | if magnitude % 3 == 0: |
||
100 | first_place = 1 |
||
101 | elif magnitude % 3 == 1: |
||
102 | first_place = 2 |
||
103 | else: |
||
104 | first_place = 5 |
||
105 | magnitude = magnitude // 3 - 1 |
||
106 | step = first_place * pow(10, magnitude) |
||
107 | first_val = ceil(min / step) * step |
||
108 | return first_val, step |
||
109 | |||
110 | def update_ticks(self): |
||
111 | self._ticks = [] |
||
112 | major, medium, minor = self.tick_length |
||
113 | if self.labels is not None and not self.auto_scale: |
||
114 | values = self.values or range(len(self.labels)) |
||
115 | for i, text in zip(values, self.labels): |
||
116 | self._ticks.append((i, text, medium, 1)) |
||
117 | else: |
||
118 | if self.scale and not self.auto_scale: |
||
119 | min, max, step = self.scale |
||
120 | elif self.auto_range: |
||
121 | min, max = self.auto_range |
||
122 | if min is not None and max is not None: |
||
123 | step = (max - min) / 10 |
||
124 | else: |
||
125 | return |
||
126 | else: |
||
127 | return |
||
128 | |||
129 | if max == min: |
||
130 | return |
||
131 | |||
132 | val, step = self.compute_scale(min, max) |
||
133 | while val <= max: |
||
134 | self._ticks.append((val, "%.4g" % val, medium, step)) |
||
135 | val += step |
||
136 | |||
137 | def update_graph(self): |
||
138 | if self.update_callback: |
||
139 | self.update_callback() |
||
140 | |||
141 | def update(self, zoom_only=False): |
||
142 | self.update_ticks() |
||
143 | line_color = self.plot.color(OWPalette.Axis) |
||
144 | text_color = self.plot.color(OWPalette.Text) |
||
145 | if not self.graph_line or not self.scene(): |
||
146 | return |
||
147 | self.line_item.setLine(self.graph_line) |
||
148 | self.line_item.setPen(line_color) |
||
149 | if self.title: |
||
150 | self.title_item.setHtml('<b>' + self.title + '</b>') |
||
151 | self.title_item.setDefaultTextColor(text_color) |
||
152 | if self.title_location == AxisMiddle: |
||
153 | title_p = 0.5 |
||
154 | elif self.title_location == AxisEnd: |
||
155 | title_p = 0.95 |
||
156 | else: |
||
157 | title_p = 0.05 |
||
158 | title_pos = self.graph_line.pointAt(title_p) |
||
159 | v = self.graph_line.normalVector().unitVector() |
||
160 | |||
161 | dense_text = False |
||
162 | if hasattr(self, 'title_margin'): |
||
163 | offset = self.title_margin |
||
164 | elif self._ticks: |
||
165 | if self.should_be_expanded(): |
||
166 | offset = 55 |
||
167 | dense_text = True |
||
168 | else: |
||
169 | offset = 35 |
||
170 | else: |
||
171 | offset = 10 |
||
172 | |||
173 | if self.title_above: |
||
174 | title_pos += (v.p2() - v.p1()) * (offset + QFontMetrics(self.title_item.font()).height()) |
||
175 | else: |
||
176 | title_pos -= (v.p2() - v.p1()) * offset |
||
177 | ## TODO: Move it according to self.label_pos |
||
178 | self.title_item.setVisible(self.show_title) |
||
179 | self.title_item.setRotation(-self.graph_line.angle()) |
||
180 | c = self.title_item.mapToParent(self.title_item.boundingRect().center()) |
||
181 | tl = self.title_item.mapToParent(self.title_item.boundingRect().topLeft()) |
||
182 | self.title_item.setPos(title_pos - c + tl) |
||
183 | |||
184 | ## Arrows |
||
185 | if not zoom_only: |
||
186 | if self.start_arrow_item: |
||
187 | self.scene().removeItem(self.start_arrow_item) |
||
188 | self.start_arrow_item = None |
||
189 | if self.end_arrow_item: |
||
190 | self.scene().removeItem(self.end_arrow_item) |
||
191 | self.end_arrow_item = None |
||
192 | |||
193 | if self.arrows & AxisStart: |
||
194 | if not zoom_only or not self.start_arrow_item: |
||
195 | self.start_arrow_item = QGraphicsPathItem(self.arrow_path, self) |
||
196 | self.start_arrow_item.setPos(self.graph_line.p1()) |
||
197 | self.start_arrow_item.setRotation(-self.graph_line.angle() + 180) |
||
198 | self.start_arrow_item.setBrush(line_color) |
||
199 | self.start_arrow_item.setPen(line_color) |
||
200 | if self.arrows & AxisEnd: |
||
201 | if not zoom_only or not self.end_arrow_item: |
||
202 | self.end_arrow_item = QGraphicsPathItem(self.arrow_path, self) |
||
203 | self.end_arrow_item.setPos(self.graph_line.p2()) |
||
204 | self.end_arrow_item.setRotation(-self.graph_line.angle()) |
||
205 | self.end_arrow_item.setBrush(line_color) |
||
206 | self.end_arrow_item.setPen(line_color) |
||
207 | |||
208 | ## Labels |
||
209 | |||
210 | n = len(self._ticks) |
||
211 | resize_plot_item_list(self.label_items, n, QGraphicsTextItem, self) |
||
212 | resize_plot_item_list(self.label_bg_items, n, QGraphicsRectItem, self) |
||
213 | resize_plot_item_list(self.tick_items, n, QGraphicsLineItem, self) |
||
214 | |||
215 | test_rect = QRectF(self.graph_line.p1(), self.graph_line.p2()).normalized() |
||
216 | test_rect.adjust(-1, -1, 1, 1) |
||
217 | |||
218 | n_v = self.graph_line.normalVector().unitVector() |
||
219 | if self.title_above: |
||
220 | n_p = n_v.p2() - n_v.p1() |
||
221 | else: |
||
222 | n_p = n_v.p1() - n_v.p2() |
||
223 | l_v = self.graph_line.unitVector() |
||
224 | l_p = l_v.p2() - l_v.p1() |
||
225 | for i in range(n): |
||
226 | pos, text, size, step = self._ticks[i] |
||
227 | hs = 0.5 * step |
||
228 | tick_pos = self.map_to_graph(pos) |
||
229 | if not test_rect.contains(tick_pos): |
||
230 | self.tick_items[i].setVisible(False) |
||
231 | self.label_items[i].setVisible(False) |
||
232 | continue |
||
233 | item = self.label_items[i] |
||
234 | item.setVisible(True) |
||
235 | if not zoom_only: |
||
236 | if self.id in XAxes or getattr(self, 'is_horizontal', False): |
||
237 | item.setHtml('<center>' + Qt.escape(text.strip()) + '</center>') |
||
238 | else: |
||
239 | item.setHtml(Qt.escape(text.strip())) |
||
240 | |||
241 | item.setTextWidth(-1) |
||
242 | text_angle = 0 |
||
243 | if dense_text: |
||
244 | w = min(item.boundingRect().width(), self.max_text_width) |
||
245 | item.setTextWidth(w) |
||
246 | if self.title_above: |
||
247 | label_pos = tick_pos + n_p * (w + self.text_margin) + l_p * item.boundingRect().height() / 2 |
||
248 | else: |
||
249 | label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height() / 2 |
||
250 | text_angle = -90 if self.title_above else 90 |
||
251 | else: |
||
252 | w = min(item.boundingRect().width(), |
||
253 | QLineF(self.map_to_graph(pos - hs), self.map_to_graph(pos + hs)).length()) |
||
254 | label_pos = tick_pos + n_p * self.text_margin + l_p * item.boundingRect().height() / 2 |
||
255 | item.setTextWidth(w) |
||
256 | |||
257 | if not self.always_horizontal_text: |
||
258 | if self.title_above: |
||
259 | item.setRotation(-self.graph_line.angle() - text_angle) |
||
260 | else: |
||
261 | item.setRotation(self.graph_line.angle() - text_angle) |
||
262 | |||
263 | item.setPos(label_pos) |
||
264 | item.setDefaultTextColor(text_color) |
||
265 | |||
266 | self.label_bg_items[i].setRect(item.boundingRect()) |
||
267 | self.label_bg_items[i].setPen(QPen(Qt.NoPen)) |
||
268 | self.label_bg_items[i].setBrush(self.plot.color(OWPalette.Canvas)) |
||
269 | |||
270 | item = self.tick_items[i] |
||
271 | item.setVisible(True) |
||
272 | tick_line = QLineF(v) |
||
273 | tick_line.translate(-tick_line.p1()) |
||
274 | tick_line.setLength(size) |
||
275 | if self.title_above: |
||
276 | tick_line.setAngle(tick_line.angle() + 180) |
||
277 | item.setLine(tick_line) |
||
278 | item.setPen(line_color) |
||
279 | item.setPos(self.map_to_graph(pos)) |
||
280 | |||
281 | @staticmethod |
||
282 | def make_title(label, unit=None): |
||
283 | lab = '<i>' + label + '</i>' |
||
284 | if unit: |
||
285 | lab = lab + ' [' + unit + ']' |
||
286 | return lab |
||
287 | |||
288 | def set_line(self, line): |
||
289 | self.graph_line = line |
||
290 | self.update() |
||
291 | |||
292 | def set_title(self, title): |
||
293 | self.title = title |
||
294 | self.update() |
||
295 | |||
296 | def set_show_title(self, b): |
||
297 | self.show_title = b |
||
298 | self.update() |
||
299 | |||
300 | def set_labels(self, labels, values): |
||
301 | self.labels = labels |
||
302 | self.values = values |
||
303 | self.graph_line = None |
||
304 | self.auto_scale = False |
||
305 | self.update_ticks() |
||
306 | self.update_graph() |
||
307 | |||
308 | def set_scale(self, min, max, step_size): |
||
309 | self.scale = (min, max, step_size) |
||
310 | self.graph_line = None |
||
311 | self.auto_scale = False |
||
312 | self.update_ticks() |
||
313 | self.update_graph() |
||
314 | |||
315 | def set_tick_length(self, minor, medium, major): |
||
316 | self.tick_length = (minor, medium, major) |
||
317 | self.update() |
||
318 | |||
319 | def map_to_graph(self, x): |
||
320 | min, max = self.plot.bounds_for_axis(self.id) |
||
321 | if min == max: |
||
322 | return QPointF() |
||
323 | line_point = self.graph_line.pointAt((x - min) / (max - min)) |
||
324 | end_point = line_point * self.zoom_transform |
||
325 | return self.projection(end_point, self.graph_line) |
||
326 | |||
327 | @staticmethod |
||
328 | def projection(point, line): |
||
329 | norm = line.normalVector() |
||
330 | norm.translate(point - norm.p1()) |
||
331 | p = QPointF() |
||
332 | type = line.intersect(norm, p) |
||
333 | return p |
||
334 | |||
335 | def continuous_labels(self): |
||
336 | min, max, step = self.scale |
||
337 | magnitude = log10(abs(max - min)) |
||
338 | |||
339 | def paint(self, painter, option, widget): |
||
340 | pass |
||
341 | |||
342 | def boundingRect(self): |
||
343 | return QRectF() |
||
344 | |||
345 | def ticks(self): |
||
346 | if not self._ticks: |
||
347 | self.update_ticks() |
||
348 | return self._ticks |
||
349 | |||
350 | def bounds(self): |
||
351 | if self._bounds: |
||
352 | return self._bounds |
||
353 | if self.labels: |
||
354 | return -0.2, len(self.labels) - 0.8 |
||
355 | elif self.scale: |
||
356 | min, max, _step = self.scale |
||
357 | return min, max |
||
358 | elif self.auto_range: |
||
359 | return self.auto_range |
||
360 | else: |
||
361 | return 0, 1 |
||
362 | |||
363 | def set_bounds(self, value): |
||
364 | self._bounds = value |
||
365 | |||
366 | def should_be_expanded(self): |
||
367 | self.update_ticks() |
||
368 | return self.id in YAxes or self.always_horizontal_text or sum( |
||
369 | len(t[1]) for t in self._ticks) * 12 > self.plot.width() |
||
370 | |||
371 |
It is generally discouraged to redefine built-ins as this makes code very hard to read.