* Copyright 2009, Ingo Weinhold, ingo_weinhold@gmx.de.
* Distributed under the terms of the MIT License.
*/
#include "chart/Chart.h"
#include <stdio.h>
#include <new>
#include <ControlLook.h>
#include <Region.h>
#include <ScrollBar.h>
#include <Window.h>
#include "chart/ChartAxis.h"
#include "chart/ChartDataSource.h"
#include "chart/ChartRenderer.h"
Chart::AxisInfo::AxisInfo()
:
axis(NULL)
{
}
void
Chart::AxisInfo::SetFrame(float left, float top, float right, float bottom)
{
frame.Set(left, top, right, bottom);
if (axis != NULL)
axis->SetFrame(frame);
}
void
Chart::AxisInfo::SetRange(const ChartDataRange& range)
{
if (axis != NULL)
axis->SetRange(range);
}
void
Chart::AxisInfo::Render(BView* view, const BRect& updateRect)
{
if (axis != NULL)
axis->Render(view, updateRect);
}
Chart::Chart(ChartRenderer* renderer, const char* name)
:
BView(name, B_FRAME_EVENTS | B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE),
fRenderer(renderer),
fHScrollSize(0),
fVScrollSize(0),
fHScrollValue(0),
fVScrollValue(0),
fIgnoreScrollEvent(0),
fDomainZoomLimit(0),
fLastMousePos(-1, -1),
fDraggingStartPos(-1, -1)
{
SetViewColor(B_TRANSPARENT_32_BIT);
}
Chart::~Chart()
{
}
bool
Chart::AddDataSource(ChartDataSource* dataSource, int32 index,
ChartRendererDataSourceConfig* config)
{
if (dataSource == NULL)
return false;
if (index < 0 || index > fDataSources.CountItems())
index = fDataSources.CountItems();
if (!fDataSources.AddItem(dataSource, index))
return false;
if (!fRenderer->AddDataSource(dataSource, index, config)) {
fDataSources.RemoveItemAt(index);
return false;
}
_UpdateDomainAndRange();
InvalidateLayout();
Invalidate();
return true;
}
bool
Chart::AddDataSource(ChartDataSource* dataSource,
ChartRendererDataSourceConfig* config)
{
return AddDataSource(dataSource, -1, config);
}
bool
Chart::RemoveDataSource(ChartDataSource* dataSource)
{
if (dataSource == NULL)
return false;
return RemoveDataSource(fDataSources.IndexOf(dataSource));
}
ChartDataSource*
Chart::RemoveDataSource(int32 index)
{
if (index < 0 || index >= fDataSources.CountItems())
return NULL;
ChartDataSource* dataSource = fDataSources.RemoveItemAt(index);
fRenderer->RemoveDataSource(dataSource);
_UpdateDomainAndRange();
Invalidate();
return dataSource;
}
void
Chart::RemoveAllDataSources()
{
int32 count = fDataSources.CountItems();
for (int32 i = count - 1; i >= 0; i--)
fRenderer->RemoveDataSource(fDataSources.ItemAt(i));
fDataSources.MakeEmpty();
_UpdateDomainAndRange();
InvalidateLayout();
Invalidate();
}
void
Chart::SetAxis(ChartAxisLocation location, ChartAxis* axis)
{
switch (location) {
case CHART_AXIS_LEFT:
fLeftAxis.axis = axis;
break;
case CHART_AXIS_TOP:
fTopAxis.axis = axis;
break;
case CHART_AXIS_RIGHT:
fRightAxis.axis = axis;
break;
case CHART_AXIS_BOTTOM:
fBottomAxis.axis = axis;
break;
default:
return;
}
axis->SetLocation(location);
InvalidateLayout();
Invalidate();
}
void
Chart::SetDisplayDomain(ChartDataRange domain)
{
if (domain.IsValid() && domain.Size() < fDomain.Size()) {
if (domain.min < fDomain.min)
domain.OffsetTo(fDomain.min);
else if (domain.max > fDomain.max)
domain.OffsetBy(fDomain.max - domain.max);
} else
domain = fDomain;
if (domain == fDisplayDomain)
return;
fDisplayDomain = domain;
fRenderer->SetDomain(fDisplayDomain);
fTopAxis.SetRange(fDisplayDomain);
fBottomAxis.SetRange(fDisplayDomain);
_UpdateScrollBar(true);
InvalidateLayout();
Invalidate();
}
void
Chart::SetDisplayRange(ChartDataRange range)
{
if (range.IsValid() && range.Size() < fRange.Size()) {
if (range.min < fRange.min)
range.OffsetTo(fRange.min);
else if (range.max > fRange.max)
range.OffsetBy(fRange.max - range.max);
} else
range = fRange;
if (range == fDisplayRange)
return;
fDisplayRange = range;
fRenderer->SetRange(fDisplayRange);
fLeftAxis.SetRange(fDisplayRange);
fRightAxis.SetRange(fDisplayRange);
_UpdateScrollBar(false);
InvalidateLayout();
Invalidate();
}
double
Chart::DomainZoomLimit() const
{
return fDomainZoomLimit;
}
void
Chart::SetDomainZoomLimit(double limit)
{
fDomainZoomLimit = limit;
}
void
Chart::DomainChanged()
{
if (ScrollBar(B_HORIZONTAL) != NULL && fDisplayDomain.IsValid())
SetDisplayDomain(fDisplayDomain);
else
SetDisplayDomain(fDomain);
}
void
Chart::RangeChanged()
{
if (ScrollBar(B_VERTICAL) != NULL && fDisplayRange.IsValid())
SetDisplayRange(fDisplayRange);
else
SetDisplayRange(fRange);
}
void
Chart::MessageReceived(BMessage* message)
{
switch (message->what) {
case B_MOUSE_WHEEL_CHANGED:
{
float deltaY;
if ((modifiers() & B_SHIFT_KEY) == 0
|| message->FindFloat("be:wheel_delta_y", &deltaY) != B_OK
|| !fChartFrame.InsetByCopy(1, 1).Contains(fLastMousePos)) {
break;
}
_Zoom(fLastMousePos.x, deltaY);
return;
}
}
BView::MessageReceived(message);
}
void
Chart::FrameResized(float newWidth, float newHeight)
{
_UpdateScrollBar(true);
_UpdateScrollBar(false);
Invalidate();
}
void
Chart::MouseDown(BPoint where)
{
if (fDraggingStartPos.x >= 0 || ScrollBar(B_HORIZONTAL) == NULL)
return;
int32 buttons;
if (Window()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK
|| (buttons & B_PRIMARY_MOUSE_BUTTON) == 0) {
return;
}
fDraggingStartPos = where;
fDraggingStartScrollValue = fHScrollValue;
SetMouseEventMask(B_POINTER_EVENTS);
}
void
Chart::MouseUp(BPoint where)
{
int32 buttons;
if (fDraggingStartPos.x < 0
|| Window()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK
|| (buttons & B_PRIMARY_MOUSE_BUTTON) != 0) {
return;
}
fDraggingStartPos.x = -1;
}
void
Chart::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage)
{
fLastMousePos = where;
if (fDraggingStartPos.x < 0)
return;
ScrollBar(B_HORIZONTAL)->SetValue(fDraggingStartScrollValue
+ fDraggingStartPos.x - where.x);
}
void
Chart::Draw(BRect updateRect)
{
rgb_color background = ui_color(B_PANEL_BACKGROUND_COLOR);
rgb_color color;
if (fLeftAxis.axis != NULL || fTopAxis.axis != NULL
|| fRightAxis.axis != NULL || fBottomAxis.axis != NULL) {
SetLowColor(background);
BRegion clippingRegion(Bounds());
clippingRegion.Exclude(fChartFrame);
ConstrainClippingRegion(&clippingRegion);
FillRect(Bounds(), B_SOLID_LOW);
ConstrainClippingRegion(NULL);
}
fLeftAxis.Render(this, updateRect);
fTopAxis.Render(this, updateRect);
fRightAxis.Render(this, updateRect);
fBottomAxis.Render(this, updateRect);
BRect chartFrame(fChartFrame);
be_control_look->DrawBorder(this, chartFrame, updateRect, background,
B_PLAIN_BORDER);
SetHighColor(color.set_to(255, 255, 255, 255));
FillRect(chartFrame);
BRegion clippingRegion(chartFrame);
ConstrainClippingRegion(&clippingRegion);
fRenderer->Render(this, updateRect);
}
void
Chart::ScrollTo(BPoint where)
{
if (fIgnoreScrollEvent > 0)
return;
_ScrollTo(where.x, true);
_ScrollTo(where.y, false);
}
BSize
Chart::MinSize()
{
return BSize(100, 100);
}
BSize
Chart::MaxSize()
{
return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
}
BSize
Chart::PreferredSize()
{
return MinSize();
}
void
Chart::DoLayout()
{
BSize size = Bounds().Size();
int32 width = size.IntegerWidth() + 1;
int32 height = size.IntegerHeight() + 1;
int32 left = 0;
int32 right = 0;
int32 top = 0;
int32 bottom = 0;
if (fLeftAxis.axis != NULL)
left = fLeftAxis.axis->PreferredSize(this, size).IntegerWidth() + 1;
if (fRightAxis.axis != NULL)
right = fRightAxis.axis->PreferredSize(this, size).IntegerWidth() + 1;
if (fTopAxis.axis != NULL)
top = fTopAxis.axis->PreferredSize(this, size).IntegerHeight() + 1;
if (fBottomAxis.axis != NULL) {
bottom = fBottomAxis.axis->PreferredSize(this, size).IntegerHeight()
+ 1;
}
fChartFrame = BRect(left, top, width - right - 1, height - bottom - 1);
fRenderer->SetFrame(fChartFrame.InsetByCopy(1, 1));
fLeftAxis.SetFrame(0, fChartFrame.top + 1, fChartFrame.left - 1,
fChartFrame.bottom - 1);
fRightAxis.SetFrame(fChartFrame.right + 1, fChartFrame.top + 1, width - 1,
fChartFrame.bottom - 1);
fTopAxis.SetFrame(fChartFrame.left + 1, 0, fChartFrame.right - 1,
fChartFrame.top - 1);
fBottomAxis.SetFrame(fChartFrame.left + 1, fChartFrame.bottom + 1,
fChartFrame.right - 1, height - 1);
}
void
Chart::_UpdateDomainAndRange()
{
ChartDataRange oldDomain = fDomain;
ChartDataRange oldRange = fRange;
if (fDataSources.IsEmpty()) {
fDomain = ChartDataRange();
fRange = ChartDataRange();
} else {
ChartDataSource* firstSource = fDataSources.ItemAt(0);
fDomain = firstSource->Domain();
fRange = firstSource->Range();
for (int32 i = 1; ChartDataSource* source = fDataSources.ItemAt(i);
i++) {
fDomain.Extend(source->Domain());
fRange.Extend(source->Range());
}
}
if (fDomain != oldDomain)
DomainChanged();
if (fRange != oldRange)
RangeChanged();
}
void
Chart::_UpdateScrollBar(bool horizontal)
{
const ChartDataRange& range = horizontal ? fDomain : fRange;
const ChartDataRange& displayRange = horizontal
? fDisplayDomain : fDisplayRange;
float chartSize = horizontal ? fChartFrame.Width() : fChartFrame.Height();
chartSize--;
float& scrollSize = horizontal ? fHScrollSize : fVScrollSize;
float& scrollValue = horizontal ? fHScrollValue : fVScrollValue;
BScrollBar* scrollBar = ScrollBar(horizontal ? B_HORIZONTAL : B_VERTICAL);
float proportion;
if (range.IsValid() && displayRange.IsValid()) {
scrollSize = (range.Size() / displayRange.Size() - 1) * chartSize;
scrollValue = (displayRange.min - range.min) / displayRange.Size()
* chartSize;
proportion = displayRange.Size() / range.Size();
} else {
scrollSize = 0;
scrollValue = 0;
proportion = 1;
}
if (scrollBar != NULL) {
fIgnoreScrollEvent++;
scrollBar->SetRange(0, scrollSize);
fIgnoreScrollEvent--;
scrollBar->SetValue(scrollValue);
scrollBar->SetProportion(proportion);
}
}
void
Chart::_ScrollTo(float value, bool horizontal)
{
float& scrollValue = horizontal ? fHScrollValue : fVScrollValue;
if (value == scrollValue)
return;
const ChartDataRange& range = horizontal ? fDomain : fRange;
ChartDataRange displayRange = horizontal ? fDisplayDomain : fDisplayRange;
float chartSize = horizontal ? fChartFrame.Width() : fChartFrame.Height();
chartSize--;
const float& scrollSize = horizontal ? fHScrollSize : fVScrollSize;
scrollValue = value;
displayRange.OffsetTo(value / scrollSize
* (range.Size() - displayRange.Size()));
if (horizontal)
SetDisplayDomain(displayRange);
else
SetDisplayRange(displayRange);
}
void
Chart::_Zoom(float x, float steps)
{
double displayDomainSize = fDisplayDomain.Size();
if (fDomainZoomLimit <= 0 || !fDomain.IsValid() || !fDisplayDomain.IsValid()
|| steps == 0) {
return;
}
float chartSize = fChartFrame.Width() - 1;
x -= fChartFrame.left + 1;
double domainPos = (fHScrollValue + x) / (fHScrollSize + chartSize)
* fDomain.Size();
double factor = 2;
if (steps < 0) {
steps = -steps;
factor = 1.0 / factor;
}
for (; steps > 0; steps--)
displayDomainSize *= factor;
if (displayDomainSize < fDomainZoomLimit)
displayDomainSize = fDomainZoomLimit;
if (displayDomainSize > fDomain.Size())
displayDomainSize = fDomain.Size();
if (displayDomainSize == fDisplayDomain.Size())
return;
domainPos -= displayDomainSize * x / chartSize;
SetDisplayDomain(ChartDataRange(domainPos, domainPos + displayDomainSize));
}