1 /*
2 * Copyright 2009, Ingo Weinhold, ingo_weinhold@gmx.de.
3 * Distributed under the terms of the MIT License.
4 */
5
6 #include "chart/Chart.h"
7
8 #include <stdio.h>
9
10 #include <new>
11
12 #include <ControlLook.h>
13 #include <Region.h>
14 #include <ScrollBar.h>
15 #include <Window.h>
16
17 #include "chart/ChartAxis.h"
18 #include "chart/ChartDataSource.h"
19 #include "chart/ChartRenderer.h"
20
21
22 // #pragma mark - Chart::AxisInfo
23
24
AxisInfo()25 Chart::AxisInfo::AxisInfo()
26 :
27 axis(NULL)
28 {
29 }
30
31
32 void
SetFrame(float left,float top,float right,float bottom)33 Chart::AxisInfo::SetFrame(float left, float top, float right, float bottom)
34 {
35 frame.Set(left, top, right, bottom);
36 if (axis != NULL)
37 axis->SetFrame(frame);
38 }
39
40
41 void
SetRange(const ChartDataRange & range)42 Chart::AxisInfo::SetRange(const ChartDataRange& range)
43 {
44 if (axis != NULL)
45 axis->SetRange(range);
46 }
47
48
49 void
Render(BView * view,const BRect & updateRect)50 Chart::AxisInfo::Render(BView* view, const BRect& updateRect)
51 {
52 if (axis != NULL)
53 axis->Render(view, updateRect);
54 }
55
56
57 // #pragma mark - Chart
58
59
Chart(ChartRenderer * renderer,const char * name)60 Chart::Chart(ChartRenderer* renderer, const char* name)
61 :
62 BView(name, B_FRAME_EVENTS | B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE),
63 fRenderer(renderer),
64 fHScrollSize(0),
65 fVScrollSize(0),
66 fHScrollValue(0),
67 fVScrollValue(0),
68 fIgnoreScrollEvent(0),
69 fDomainZoomLimit(0),
70 fLastMousePos(-1, -1),
71 fDraggingStartPos(-1, -1)
72 {
73 SetViewColor(B_TRANSPARENT_32_BIT);
74
75 // fRenderer->SetFrame(Bounds());
76 }
77
78
~Chart()79 Chart::~Chart()
80 {
81 }
82
83
84 bool
AddDataSource(ChartDataSource * dataSource,int32 index,ChartRendererDataSourceConfig * config)85 Chart::AddDataSource(ChartDataSource* dataSource, int32 index,
86 ChartRendererDataSourceConfig* config)
87 {
88 if (dataSource == NULL)
89 return false;
90
91 if (index < 0 || index > fDataSources.CountItems())
92 index = fDataSources.CountItems();
93
94 if (!fDataSources.AddItem(dataSource, index))
95 return false;
96
97 if (!fRenderer->AddDataSource(dataSource, index, config)) {
98 fDataSources.RemoveItemAt(index);
99 return false;
100 }
101
102 _UpdateDomainAndRange();
103
104 InvalidateLayout();
105 Invalidate();
106
107 return true;
108 }
109
110
111 bool
AddDataSource(ChartDataSource * dataSource,ChartRendererDataSourceConfig * config)112 Chart::AddDataSource(ChartDataSource* dataSource,
113 ChartRendererDataSourceConfig* config)
114 {
115 return AddDataSource(dataSource, -1, config);
116 }
117
118
119 bool
RemoveDataSource(ChartDataSource * dataSource)120 Chart::RemoveDataSource(ChartDataSource* dataSource)
121 {
122 if (dataSource == NULL)
123 return false;
124
125 return RemoveDataSource(fDataSources.IndexOf(dataSource));
126 }
127
128
129 ChartDataSource*
RemoveDataSource(int32 index)130 Chart::RemoveDataSource(int32 index)
131 {
132 if (index < 0 || index >= fDataSources.CountItems())
133 return NULL;
134
135 ChartDataSource* dataSource = fDataSources.RemoveItemAt(index);
136
137 fRenderer->RemoveDataSource(dataSource);
138
139 _UpdateDomainAndRange();
140
141 Invalidate();
142
143 return dataSource;
144 }
145
146
147 void
RemoveAllDataSources()148 Chart::RemoveAllDataSources()
149 {
150 int32 count = fDataSources.CountItems();
151 for (int32 i = count - 1; i >= 0; i--)
152 fRenderer->RemoveDataSource(fDataSources.ItemAt(i));
153
154 fDataSources.MakeEmpty();
155
156 _UpdateDomainAndRange();
157
158 InvalidateLayout();
159 Invalidate();
160 }
161
162
163 void
SetAxis(ChartAxisLocation location,ChartAxis * axis)164 Chart::SetAxis(ChartAxisLocation location, ChartAxis* axis)
165 {
166 switch (location) {
167 case CHART_AXIS_LEFT:
168 fLeftAxis.axis = axis;
169 break;
170 case CHART_AXIS_TOP:
171 fTopAxis.axis = axis;
172 break;
173 case CHART_AXIS_RIGHT:
174 fRightAxis.axis = axis;
175 break;
176 case CHART_AXIS_BOTTOM:
177 fBottomAxis.axis = axis;
178 break;
179 default:
180 return;
181 }
182
183 axis->SetLocation(location);
184
185 InvalidateLayout();
186 Invalidate();
187 }
188
189
190 void
SetDisplayDomain(ChartDataRange domain)191 Chart::SetDisplayDomain(ChartDataRange domain)
192 {
193 // sanitize the supplied range
194 if (domain.IsValid() && domain.Size() < fDomain.Size()) {
195 if (domain.min < fDomain.min)
196 domain.OffsetTo(fDomain.min);
197 else if (domain.max > fDomain.max)
198 domain.OffsetBy(fDomain.max - domain.max);
199 } else
200 domain = fDomain;
201
202 if (domain == fDisplayDomain)
203 return;
204
205 fDisplayDomain = domain;
206
207 fRenderer->SetDomain(fDisplayDomain);
208 fTopAxis.SetRange(fDisplayDomain);
209 fBottomAxis.SetRange(fDisplayDomain);
210
211 _UpdateScrollBar(true);
212
213 InvalidateLayout();
214 Invalidate();
215 }
216
217
218 void
SetDisplayRange(ChartDataRange range)219 Chart::SetDisplayRange(ChartDataRange range)
220 {
221 // sanitize the supplied range
222 if (range.IsValid() && range.Size() < fRange.Size()) {
223 if (range.min < fRange.min)
224 range.OffsetTo(fRange.min);
225 else if (range.max > fRange.max)
226 range.OffsetBy(fRange.max - range.max);
227 } else
228 range = fRange;
229
230 if (range == fDisplayRange)
231 return;
232
233 fDisplayRange = range;
234
235 fRenderer->SetRange(fDisplayRange);
236 fLeftAxis.SetRange(fDisplayRange);
237 fRightAxis.SetRange(fDisplayRange);
238
239 _UpdateScrollBar(false);
240
241 InvalidateLayout();
242 Invalidate();
243 }
244
245
246 double
DomainZoomLimit() const247 Chart::DomainZoomLimit() const
248 {
249 return fDomainZoomLimit;
250 }
251
252
253 void
SetDomainZoomLimit(double limit)254 Chart::SetDomainZoomLimit(double limit)
255 {
256 fDomainZoomLimit = limit;
257 }
258
259
260 void
DomainChanged()261 Chart::DomainChanged()
262 {
263 if (ScrollBar(B_HORIZONTAL) != NULL && fDisplayDomain.IsValid())
264 SetDisplayDomain(fDisplayDomain);
265 else
266 SetDisplayDomain(fDomain);
267 }
268
269
270 void
RangeChanged()271 Chart::RangeChanged()
272 {
273 if (ScrollBar(B_VERTICAL) != NULL && fDisplayRange.IsValid())
274 SetDisplayRange(fDisplayRange);
275 else
276 SetDisplayRange(fRange);
277 }
278
279
280 void
MessageReceived(BMessage * message)281 Chart::MessageReceived(BMessage* message)
282 {
283 switch (message->what) {
284 case B_MOUSE_WHEEL_CHANGED:
285 {
286 // We're only interested in Shift + vertical wheel, if the mouse
287 // is in the chart frame.
288 float deltaY;
289 if ((modifiers() & B_SHIFT_KEY) == 0
290 || message->FindFloat("be:wheel_delta_y", &deltaY) != B_OK
291 || !fChartFrame.InsetByCopy(1, 1).Contains(fLastMousePos)) {
292 break;
293 }
294
295 _Zoom(fLastMousePos.x, deltaY);
296
297 return;
298 }
299 }
300
301 BView::MessageReceived(message);
302 }
303
304
305 void
FrameResized(float newWidth,float newHeight)306 Chart::FrameResized(float newWidth, float newHeight)
307 {
308 //printf("Chart::FrameResized(%f, %f)\n", newWidth, newHeight);
309 // fRenderer->SetFrame(Bounds());
310
311 _UpdateScrollBar(true);
312 _UpdateScrollBar(false);
313
314 Invalidate();
315 }
316
317
318 void
MouseDown(BPoint where)319 Chart::MouseDown(BPoint where)
320 {
321 // ignore, if already dragging or if there's no scrollbar
322 if (fDraggingStartPos.x >= 0 || ScrollBar(B_HORIZONTAL) == NULL)
323 return;
324
325 // the first button must be pressed
326 int32 buttons;
327 if (Window()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK
328 || (buttons & B_PRIMARY_MOUSE_BUTTON) == 0) {
329 return;
330 }
331
332 fDraggingStartPos = where;
333 fDraggingStartScrollValue = fHScrollValue;
334
335 SetMouseEventMask(B_POINTER_EVENTS);
336 }
337
338
339 void
MouseUp(BPoint where)340 Chart::MouseUp(BPoint where)
341 {
342 // ignore if not dragging, or if the first mouse button is still pressed
343 int32 buttons;
344 if (fDraggingStartPos.x < 0
345 || Window()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK
346 || (buttons & B_PRIMARY_MOUSE_BUTTON) != 0) {
347 return;
348 }
349
350 // stop dragging
351 fDraggingStartPos.x = -1;
352 }
353
354
355 void
MouseMoved(BPoint where,uint32 code,const BMessage * dragMessage)356 Chart::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage)
357 {
358 fLastMousePos = where;
359
360 if (fDraggingStartPos.x < 0)
361 return;
362
363 ScrollBar(B_HORIZONTAL)->SetValue(fDraggingStartScrollValue
364 + fDraggingStartPos.x - where.x);
365 }
366
367
368 void
Draw(BRect updateRect)369 Chart::Draw(BRect updateRect)
370 {
371 //printf("Chart::Draw((%f, %f) - (%f, %f))\n", updateRect.left, updateRect.top, updateRect.right, updateRect.bottom);
372 rgb_color background = ui_color(B_PANEL_BACKGROUND_COLOR);
373 rgb_color color;
374
375 // clear the axes background
376 if (fLeftAxis.axis != NULL || fTopAxis.axis != NULL
377 || fRightAxis.axis != NULL || fBottomAxis.axis != NULL) {
378 SetLowColor(background);
379 BRegion clippingRegion(Bounds());
380 clippingRegion.Exclude(fChartFrame);
381 ConstrainClippingRegion(&clippingRegion);
382 FillRect(Bounds(), B_SOLID_LOW);
383 ConstrainClippingRegion(NULL);
384 }
385
386 // render the axes
387 fLeftAxis.Render(this, updateRect);
388 fTopAxis.Render(this, updateRect);
389 fRightAxis.Render(this, updateRect);
390 fBottomAxis.Render(this, updateRect);
391
392 // draw the frame around the chart area and clear the background
393 BRect chartFrame(fChartFrame);
394 be_control_look->DrawBorder(this, chartFrame, updateRect, background,
395 B_PLAIN_BORDER);
396 // DrawBorder() insets the supplied rect
397 SetHighColor(color.set_to(255, 255, 255, 255));
398 FillRect(chartFrame);
399
400 // render the chart
401 BRegion clippingRegion(chartFrame);
402 ConstrainClippingRegion(&clippingRegion);
403 fRenderer->Render(this, updateRect);
404 }
405
406
407 void
ScrollTo(BPoint where)408 Chart::ScrollTo(BPoint where)
409 {
410 if (fIgnoreScrollEvent > 0)
411 return;
412
413 _ScrollTo(where.x, true);
414 _ScrollTo(where.y, false);
415 }
416
417
418 BSize
MinSize()419 Chart::MinSize()
420 {
421 // TODO: Implement for real!
422 return BSize(100, 100);
423 }
424
425
426 BSize
MaxSize()427 Chart::MaxSize()
428 {
429 // TODO: Implement for real!
430 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
431 }
432
433
434 BSize
PreferredSize()435 Chart::PreferredSize()
436 {
437 // TODO: Implement for real!
438 return MinSize();
439 }
440
441
442 void
DoLayout()443 Chart::DoLayout()
444 {
445 // get size in pixels
446 BSize size = Bounds().Size();
447 //printf("Chart::DoLayout(%f, %f)\n", size.width, size.height);
448 int32 width = size.IntegerWidth() + 1;
449 int32 height = size.IntegerHeight() + 1;
450
451 // compute the axis insets
452 int32 left = 0;
453 int32 right = 0;
454 int32 top = 0;
455 int32 bottom = 0;
456
457 if (fLeftAxis.axis != NULL)
458 left = fLeftAxis.axis->PreferredSize(this, size).IntegerWidth() + 1;
459 if (fRightAxis.axis != NULL)
460 right = fRightAxis.axis->PreferredSize(this, size).IntegerWidth() + 1;
461 if (fTopAxis.axis != NULL)
462 top = fTopAxis.axis->PreferredSize(this, size).IntegerHeight() + 1;
463 if (fBottomAxis.axis != NULL) {
464 bottom = fBottomAxis.axis->PreferredSize(this, size).IntegerHeight()
465 + 1;
466 }
467
468 fChartFrame = BRect(left, top, width - right - 1, height - bottom - 1);
469 fRenderer->SetFrame(fChartFrame.InsetByCopy(1, 1));
470 //printf(" fChartFrame: (%f, %f) - (%f, %f)\n", fChartFrame.left, fChartFrame.top, fChartFrame.right, fChartFrame.bottom);
471
472 fLeftAxis.SetFrame(0, fChartFrame.top + 1, fChartFrame.left - 1,
473 fChartFrame.bottom - 1);
474 fRightAxis.SetFrame(fChartFrame.right + 1, fChartFrame.top + 1, width - 1,
475 fChartFrame.bottom - 1);
476 fTopAxis.SetFrame(fChartFrame.left + 1, 0, fChartFrame.right - 1,
477 fChartFrame.top - 1);
478 fBottomAxis.SetFrame(fChartFrame.left + 1, fChartFrame.bottom + 1,
479 fChartFrame.right - 1, height - 1);
480 }
481
482
483 void
_UpdateDomainAndRange()484 Chart::_UpdateDomainAndRange()
485 {
486 ChartDataRange oldDomain = fDomain;
487 ChartDataRange oldRange = fRange;
488
489 if (fDataSources.IsEmpty()) {
490 fDomain = ChartDataRange();
491 fRange = ChartDataRange();
492 } else {
493 ChartDataSource* firstSource = fDataSources.ItemAt(0);
494 fDomain = firstSource->Domain();
495 fRange = firstSource->Range();
496
497 for (int32 i = 1; ChartDataSource* source = fDataSources.ItemAt(i);
498 i++) {
499 fDomain.Extend(source->Domain());
500 fRange.Extend(source->Range());
501 }
502 }
503
504 if (fDomain != oldDomain)
505 DomainChanged();
506 if (fRange != oldRange)
507 RangeChanged();
508 }
509
510
511 void
_UpdateScrollBar(bool horizontal)512 Chart::_UpdateScrollBar(bool horizontal)
513 {
514 const ChartDataRange& range = horizontal ? fDomain : fRange;
515 const ChartDataRange& displayRange = horizontal
516 ? fDisplayDomain : fDisplayRange;
517 float chartSize = horizontal ? fChartFrame.Width() : fChartFrame.Height();
518 chartSize--; // +1 for pixel size, -2 for border
519 float& scrollSize = horizontal ? fHScrollSize : fVScrollSize;
520 float& scrollValue = horizontal ? fHScrollValue : fVScrollValue;
521 BScrollBar* scrollBar = ScrollBar(horizontal ? B_HORIZONTAL : B_VERTICAL);
522
523 float proportion;
524
525 if (range.IsValid() && displayRange.IsValid()) {
526 scrollSize = (range.Size() / displayRange.Size() - 1) * chartSize;
527 scrollValue = (displayRange.min - range.min) / displayRange.Size()
528 * chartSize;
529 proportion = displayRange.Size() / range.Size();
530 } else {
531 scrollSize = 0;
532 scrollValue = 0;
533 proportion = 1;
534 }
535
536 if (scrollBar != NULL) {
537 fIgnoreScrollEvent++;
538 scrollBar->SetRange(0, scrollSize);
539 fIgnoreScrollEvent--;
540 scrollBar->SetValue(scrollValue);
541 scrollBar->SetProportion(proportion);
542 }
543 }
544
545
546 void
_ScrollTo(float value,bool horizontal)547 Chart::_ScrollTo(float value, bool horizontal)
548 {
549 float& scrollValue = horizontal ? fHScrollValue : fVScrollValue;
550 if (value == scrollValue)
551 return;
552
553 const ChartDataRange& range = horizontal ? fDomain : fRange;
554 ChartDataRange displayRange = horizontal ? fDisplayDomain : fDisplayRange;
555 float chartSize = horizontal ? fChartFrame.Width() : fChartFrame.Height();
556 chartSize--; // +1 for pixel size, -2 for border
557 const float& scrollSize = horizontal ? fHScrollSize : fVScrollSize;
558
559 scrollValue = value;
560 displayRange.OffsetTo(value / scrollSize
561 * (range.Size() - displayRange.Size()));
562 if (horizontal)
563 SetDisplayDomain(displayRange);
564 else
565 SetDisplayRange(displayRange);
566 }
567
568
569 void
_Zoom(float x,float steps)570 Chart::_Zoom(float x, float steps)
571 {
572 double displayDomainSize = fDisplayDomain.Size();
573 if (fDomainZoomLimit <= 0 || !fDomain.IsValid() || !fDisplayDomain.IsValid()
574 || steps == 0) {
575 return;
576 }
577
578 // compute the domain point where to zoom in
579 float chartSize = fChartFrame.Width() - 1;
580 x -= fChartFrame.left + 1;
581 double domainPos = (fHScrollValue + x) / (fHScrollSize + chartSize)
582 * fDomain.Size();
583
584 double factor = 2;
585 if (steps < 0) {
586 steps = -steps;
587 factor = 1.0 / factor;
588 }
589
590 for (; steps > 0; steps--)
591 displayDomainSize *= factor;
592
593 if (displayDomainSize < fDomainZoomLimit)
594 displayDomainSize = fDomainZoomLimit;
595 if (displayDomainSize > fDomain.Size())
596 displayDomainSize = fDomain.Size();
597
598 if (displayDomainSize == fDisplayDomain.Size())
599 return;
600
601 domainPos -= displayDomainSize * x / chartSize;
602 SetDisplayDomain(ChartDataRange(domainPos, domainPos + displayDomainSize));
603 // will adjust the supplied display domain to fit the domain
604 }
605