⛏️ index : haiku.git

/*
 * 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"


// #pragma mark - Chart::AxisInfo


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);
}


// #pragma mark - Chart


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);

//	fRenderer->SetFrame(Bounds());
}


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)
{
	// sanitize the supplied range
	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)
{
	// sanitize the supplied 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:
		{
			// We're only interested in Shift + vertical wheel, if the mouse
			// is in the chart frame.
			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)
{
//printf("Chart::FrameResized(%f, %f)\n", newWidth, newHeight);
//	fRenderer->SetFrame(Bounds());

	_UpdateScrollBar(true);
	_UpdateScrollBar(false);

	Invalidate();
}


void
Chart::MouseDown(BPoint where)
{
	// ignore, if already dragging or if there's no scrollbar
	if (fDraggingStartPos.x >= 0 || ScrollBar(B_HORIZONTAL) == NULL)
		return;

	// the first button must be pressed
	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)
{
	// ignore if not dragging, or if the first mouse button is still pressed
	int32 buttons;
	if (fDraggingStartPos.x < 0
		|| Window()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK
		|| (buttons & B_PRIMARY_MOUSE_BUTTON) != 0) {
		return;
	}

	// stop dragging
	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)
{
//printf("Chart::Draw((%f, %f) - (%f, %f))\n", updateRect.left, updateRect.top, updateRect.right, updateRect.bottom);
	rgb_color background = ui_color(B_PANEL_BACKGROUND_COLOR);
	rgb_color color;

	// clear the axes background
	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);
	}

	// render the axes
	fLeftAxis.Render(this, updateRect);
	fTopAxis.Render(this, updateRect);
	fRightAxis.Render(this, updateRect);
	fBottomAxis.Render(this, updateRect);

	// draw the frame around the chart area and clear the background
	BRect chartFrame(fChartFrame);
	be_control_look->DrawBorder(this, chartFrame, updateRect, background,
		B_PLAIN_BORDER);
		// DrawBorder() insets the supplied rect
	SetHighColor(color.set_to(255, 255, 255, 255));
	FillRect(chartFrame);

	// render the chart
	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()
{
	// TODO: Implement for real!
	return BSize(100, 100);
}


BSize
Chart::MaxSize()
{
	// TODO: Implement for real!
	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
}


BSize
Chart::PreferredSize()
{
	// TODO: Implement for real!
	return MinSize();
}


void
Chart::DoLayout()
{
	// get size in pixels
	BSize size = Bounds().Size();
//printf("Chart::DoLayout(%f, %f)\n", size.width, size.height);
	int32 width = size.IntegerWidth() + 1;
	int32 height = size.IntegerHeight() + 1;

	// compute the axis insets
	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));
//printf("  fChartFrame: (%f, %f) - (%f, %f)\n", fChartFrame.left, fChartFrame.top, fChartFrame.right, fChartFrame.bottom);

	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--;	// +1 for pixel size, -2 for border
	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--;	// +1 for pixel size, -2 for border
	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;
	}

	// compute the domain point where to zoom in
	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));
		// will adjust the supplied display domain to fit the domain
}