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
25Chart::AxisInfo::AxisInfo()
26	:
27	axis(NULL)
28{
29}
30
31
32void
33Chart::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
41void
42Chart::AxisInfo::SetRange(const ChartDataRange& range)
43{
44	if (axis != NULL)
45		axis->SetRange(range);
46}
47
48
49void
50Chart::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
60Chart::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
79Chart::~Chart()
80{
81}
82
83
84bool
85Chart::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
111bool
112Chart::AddDataSource(ChartDataSource* dataSource,
113	ChartRendererDataSourceConfig* config)
114{
115	return AddDataSource(dataSource, -1, config);
116}
117
118
119bool
120Chart::RemoveDataSource(ChartDataSource* dataSource)
121{
122	if (dataSource == NULL)
123		return false;
124
125	return RemoveDataSource(fDataSources.IndexOf(dataSource));
126}
127
128
129ChartDataSource*
130Chart::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
147void
148Chart::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
163void
164Chart::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
190void
191Chart::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
218void
219Chart::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
246double
247Chart::DomainZoomLimit() const
248{
249	return fDomainZoomLimit;
250}
251
252
253void
254Chart::SetDomainZoomLimit(double limit)
255{
256	fDomainZoomLimit = limit;
257}
258
259
260void
261Chart::DomainChanged()
262{
263	if (ScrollBar(B_HORIZONTAL) != NULL && fDisplayDomain.IsValid())
264		SetDisplayDomain(fDisplayDomain);
265	else
266		SetDisplayDomain(fDomain);
267}
268
269
270void
271Chart::RangeChanged()
272{
273	if (ScrollBar(B_VERTICAL) != NULL && fDisplayRange.IsValid())
274		SetDisplayRange(fDisplayRange);
275	else
276		SetDisplayRange(fRange);
277}
278
279
280void
281Chart::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
305void
306Chart::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
318void
319Chart::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
339void
340Chart::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
355void
356Chart::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
368void
369Chart::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
407void
408Chart::ScrollTo(BPoint where)
409{
410	if (fIgnoreScrollEvent > 0)
411		return;
412
413	_ScrollTo(where.x, true);
414	_ScrollTo(where.y, false);
415}
416
417
418BSize
419Chart::MinSize()
420{
421	// TODO: Implement for real!
422	return BSize(100, 100);
423}
424
425
426BSize
427Chart::MaxSize()
428{
429	// TODO: Implement for real!
430	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
431}
432
433
434BSize
435Chart::PreferredSize()
436{
437	// TODO: Implement for real!
438	return MinSize();
439}
440
441
442void
443Chart::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
483void
484Chart::_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
511void
512Chart::_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
546void
547Chart::_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
569void
570Chart::_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