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 25 Chart::AxisInfo::AxisInfo() 26 : 27 axis(NULL) 28 { 29 } 30 31 32 void 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 42 Chart::AxisInfo::SetRange(const ChartDataRange& range) 43 { 44 if (axis != NULL) 45 axis->SetRange(range); 46 } 47 48 49 void 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 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 79 Chart::~Chart() 80 { 81 } 82 83 84 bool 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 112 Chart::AddDataSource(ChartDataSource* dataSource, 113 ChartRendererDataSourceConfig* config) 114 { 115 return AddDataSource(dataSource, -1, config); 116 } 117 118 119 bool 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* 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 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 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 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 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 247 Chart::DomainZoomLimit() const 248 { 249 return fDomainZoomLimit; 250 } 251 252 253 void 254 Chart::SetDomainZoomLimit(double limit) 255 { 256 fDomainZoomLimit = limit; 257 } 258 259 260 void 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 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 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 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 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 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 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 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 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 419 Chart::MinSize() 420 { 421 // TODO: Implement for real! 422 return BSize(100, 100); 423 } 424 425 426 BSize 427 Chart::MaxSize() 428 { 429 // TODO: Implement for real! 430 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 431 } 432 433 434 BSize 435 Chart::PreferredSize() 436 { 437 // TODO: Implement for real! 438 return MinSize(); 439 } 440 441 442 void 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 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 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 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 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