1 // Written in the D programming language.
2 
3 /**
4 This module contains charts widgets implementation.
5 Currently only SimpleBarChart.
6 
7 
8 Synopsis:
9 
10 ----
11 import dlangui.widgets.charts;
12 
13 // creation of simple bar chart
14 SimpleBarChart chart = new SimpleBarChart("chart");
15 
16 // add bars
17 chart.addBar(12.2, makeRGBA(255, 0, 0, 0), "new bar"c);
18 
19 // update bar with index 0
20 chart.updateBar(0, 10, makeRGBA(255, 255, 0, 0), "new bar updated"c);
21 chart.updateBar(0, 20);
22 
23 // remove bars with index 0
24 chart.removeBar(0, 20);
25 
26 // change title
27 chart.title = "new title"d;
28 
29 // change min axis ratio
30 chart.axisRatio = 0.3; // y axis length will be 0.3 of x axis
31 
32 ----
33 
34 Copyright: Andrzej Kilijański, 2017
35 License:   Boost License 1.0
36 Authors:   Andrzej Kilijański, and3md@gmail.com
37 */
38 
39 module dlangui.widgets.charts;
40 
41 import dlangui.widgets.widget;
42 import std.math;
43 import std.algorithm.comparison;
44 import std.algorithm : remove;
45 import std.conv;
46 
47 class SimpleBarChart : Widget {
48 
49     this(string ID = null) {
50         super(ID);
51         clickable = false;
52         focusable = false;
53         trackHover = false;
54         styleId = "SIMPLE_BAR_CHART";
55         _axisX.arrowSize = 1;
56         title =  UIString.fromId("TITLE_NEW_CHART"c);
57         measureTextToSetWidgetSize();
58     }
59 
60     this(string ID, string titleResourceId) {
61         this(ID);
62         title = UIString.fromId(titleResourceId);
63     }
64 
65     this(string ID, dstring title) {
66         this(ID);
67         this.title = UIString.fromRaw(title);
68     }
69 
70     this(string ID, UIString title) {
71         this(ID);
72         this.title = title;
73     }
74 
75     struct BarData {
76         double y;
77         UIString title;
78         private Point _titleSize;
79         uint color;
80 
81         this (double y, uint color, UIString title) {
82             this.y = y;
83             this.color = color;
84             this.title = title;
85         }
86     }
87 
88     protected BarData[] _bars;
89     protected double _maxY = 0;
90 
91     size_t barCount() {
92         return _bars.length;
93     }
94 
95     void addBar(double y, uint color, UIString barTitle) {
96         if (y < 0)
97             return; // current limitation only positive values
98         _bars ~= BarData(y, color, barTitle);
99         if (y > _maxY)
100             _maxY = y;
101         requestLayout();
102     }
103 
104     void addBar(double y, uint color, string barTitle) {
105         addBar(y, color, UIString.fromId(barTitle));
106     }
107 
108     void addBar(double y, uint color, dstring barTitle) {
109         addBar(y, color, UIString.fromRaw(barTitle));
110     }
111 
112     void removeBar(size_t index) {
113         _bars = remove(_bars, index);
114         // update _maxY
115         _maxY = 0;
116         foreach (ref bar ; _bars) {
117             if (bar.y > _maxY)
118                 _maxY = bar.y;
119         }
120         requestLayout();
121     }
122     
123     void removeAllBars() {
124     	_bars = [];
125     	_maxY = 0;
126     	requestLayout();
127     }
128 
129     void updateBar(size_t index, double y, uint color, string barTitle) {
130         updateBar(index, y, color, UIString.fromId(barTitle));
131     }
132 
133     void updateBar(size_t index, double y, uint color, dstring barTitle) {
134         updateBar(index, y, color, UIString.fromRaw(barTitle));
135     }
136 
137     void updateBar(size_t index, double y, uint color, UIString barTitle) {
138         if (y < 0)
139             return; // current limitation only positive values
140         _bars[index].y = y;
141         _bars[index].color = color;
142         _bars[index].title = barTitle;
143 
144         // update _maxY
145         _maxY = 0;
146         foreach (ref bar ; _bars) {
147             if (bar.y > _maxY)
148                 _maxY = bar.y;
149         }
150         requestLayout();
151     }
152 
153     void updateBar(size_t index, double y) {
154         if (y < 0)
155             return; // curent limitation only positive values
156         _bars[index].y = y;
157 
158         // update _maxY
159         _maxY = 0;
160         foreach (ref bar ; _bars) {
161             if (bar.y > _maxY)
162                 _maxY = bar.y;
163         }
164         requestLayout();
165     }
166 
167     protected UIString _title;
168     protected bool _showTitle = true;
169     protected Point _titleSize;
170     protected int _marginAfterTitle = 2;
171 
172     /// set title to show
173     @property Widget title(string s) {
174         return title(UIString.fromId(s));
175     }
176 
177     @property Widget title(dstring s) {
178         return title(UIString.fromRaw(s));
179     }
180 
181     /// set title to show
182     @property Widget title(UIString s) {
183         _title = s;
184         measureTitleSize();
185         if (_showTitle)
186             requestLayout();
187         return this;
188     }
189 
190     /// get title value
191     @property dstring title() {
192         return _title;
193     }
194 
195     /// show title?
196     @property bool showTitle() {
197         return _showTitle;
198     }
199 
200     @property void showTitle(bool show) {
201         if (_showTitle != show) {
202             _showTitle = show;
203             requestLayout();
204         }
205     }
206 
207     override protected void handleFontChanged() {
208         measureTitleSize();
209         measureTextToSetWidgetSize();
210     }
211 
212     protected void measureTitleSize() {
213         FontRef font = font();
214         _titleSize = font.textSize(_title, MAX_WIDTH_UNSPECIFIED, 4, 0, textFlags); //todo: more than one line title support
215     }
216 
217     @property uint chartBackgroundColor() {return ownStyle.customColor("chart_background_color"); }
218 
219     @property Widget chartBackgroundColor(uint newColor) {
220         ownStyle.setCustomColor("chart_background_color",newColor);
221         invalidate();
222         return this;
223     }
224 
225     @property uint chartAxisColor() {return ownStyle.customColor("chart_axis_color"); }
226 
227     @property Widget chartAxisColor(uint newColor) {
228         ownStyle.setCustomColor("chart_axis_color",newColor);
229         invalidate();
230         return this;
231     }
232 
233     @property uint chartSegmentTagColor() {return ownStyle.customColor("chart_segment_tag_color"); }
234 
235     @property Widget chartSegmentTagColor(uint newColor) {
236         ownStyle.setCustomColor("chart_segment_tag_color",newColor);
237         invalidate();
238         return this;
239     }
240 
241     struct AxisData {
242         Point maxDescriptionSize = Point(30,20);
243         int thickness = 1;
244         int arrowSize = 20;
245         int segmentTagLength = 4;
246         int zeroValueDist = 3;
247         int lengthFromZeroToArrow = 200;
248     }
249 
250     AxisData _axisX;
251     AxisData _axisY;
252 
253     protected int _axisYMaxValueDescWidth = 30;
254     protected int _axisYAvgValueDescWidth = 30;
255 
256     protected double _axisRatio = 0.6;
257 
258     @property double axisRatio() {
259         return _axisRatio;
260     }
261 
262     @property void axisRatio(double newRatio) {
263         _axisRatio = newRatio;
264         requestLayout();
265     }
266 
267     protected int _minBarWidth = 10;
268     protected int _barWidth = 10;
269     protected int _barDistance = 3;
270 
271     protected int _axisXMinWfromZero = 150;
272     protected int _axisYMinDescWidth = 30;
273 
274     protected dstring _textToSetDescLineSize = "aaaaaaaaaa";
275     protected Point _measuredTextToSetDescLineSize;
276 
277     @property dstring textToSetDescLineSize() {
278         return _textToSetDescLineSize;
279     }
280 
281     @property void textToSetDescLineSize(dstring newText) {
282         _textToSetDescLineSize = newText;
283         measureTextToSetWidgetSize();
284         requestLayout();
285     }
286 
287     private int[] _charWidths;
288     protected Point measureTextToSetWidgetSize() {
289         FontRef font = font();
290         _charWidths.length = _textToSetDescLineSize.length;
291         int charsMeasured = font.measureText(_textToSetDescLineSize, _charWidths, MAX_WIDTH_UNSPECIFIED, 4);
292         _measuredTextToSetDescLineSize.x = charsMeasured > 0 ? _charWidths[charsMeasured - 1]: 0;
293         _measuredTextToSetDescLineSize.y = font.height;
294         return _measuredTextToSetDescLineSize;
295     }
296 
297     override void measure(int parentWidth, int parentHeight) {
298         FontRef font = font();
299 
300         int mWidth = minWidth;
301         int mHeight = minHeight;
302 
303         int chartW = 0;
304         int chartH = 0;
305 
306         _axisY.maxDescriptionSize = measureAxisYDesc();
307 
308         int usedWidth = _axisY.maxDescriptionSize.x + _axisY.thickness + _axisY.segmentTagLength + _axisX.zeroValueDist + margins.left + padding.left + margins.right + padding.right + _axisX.arrowSize;
309 
310         int currentMinBarWidth = max(_minBarWidth, _measuredTextToSetDescLineSize.x);
311         _axisX.maxDescriptionSize.y = _measuredTextToSetDescLineSize.y;
312 
313         // axis length
314         _axisX.lengthFromZeroToArrow = cast(uint) barCount * (currentMinBarWidth + _barDistance);
315 
316         if (_axisX.lengthFromZeroToArrow < _axisXMinWfromZero) {
317             _axisX.lengthFromZeroToArrow = _axisXMinWfromZero;
318             if (barCount > 0)
319                 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
320         }
321 
322         // minWidth and minHeight check
323 
324         if (minWidth > _axisX.lengthFromZeroToArrow + usedWidth) {
325             _axisX.lengthFromZeroToArrow = minWidth-usedWidth;
326             if (barCount > 0)
327                 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
328         }
329 
330         // width FILL_PARENT support
331         if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT) {
332             if (_axisX.lengthFromZeroToArrow < parentWidth - usedWidth) {
333                 _axisX.lengthFromZeroToArrow = parentWidth - usedWidth;
334             if (barCount > 0)
335                 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
336             }
337         }
338 
339 
340         // initialize axis y length
341         _axisY.lengthFromZeroToArrow = cast(int) round(_axisRatio * _axisX.lengthFromZeroToArrow);
342 
343         // is axis Y enought long
344         int usedHeight = _axisX.maxDescriptionSize.y + _axisX.thickness + _axisX.segmentTagLength + _axisY.zeroValueDist + ((_showTitle) ? _titleSize.y + _marginAfterTitle : 0) + margins.top + padding.top + margins.bottom + padding.bottom + _axisY.arrowSize;
345         if (minHeight > _axisY.lengthFromZeroToArrow + usedHeight) {
346             _axisY.lengthFromZeroToArrow = minHeight - usedHeight;
347             _axisX.lengthFromZeroToArrow = cast (int) round(_axisY.lengthFromZeroToArrow / _axisRatio);
348         }
349 
350         // height FILL_PARENT support
351         if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT) {
352             if (_axisY.lengthFromZeroToArrow < parentHeight - usedHeight)
353                 _axisY.lengthFromZeroToArrow = parentHeight - usedHeight;
354         }
355 
356         if (barCount > 0)
357             _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
358 
359         // compute X axis max description height
360         _axisX.maxDescriptionSize = measureAxisXDesc();
361 
362         // compute chart size
363         chartW = _axisY.maxDescriptionSize.x + _axisY.thickness + _axisY.segmentTagLength + _axisX.zeroValueDist + _axisX.lengthFromZeroToArrow + _axisX.arrowSize;
364         if (_showTitle && chartW < _titleSize.y)
365             chartW = _titleSize.y;
366 
367         chartH = _axisX.maxDescriptionSize.y + _axisX.thickness + _axisX.segmentTagLength + _axisY.zeroValueDist + _axisY.lengthFromZeroToArrow + ((_showTitle) ? _titleSize.y + _marginAfterTitle : 0) + _axisY.arrowSize;
368         measuredContent(parentWidth, parentHeight, chartW, chartH);
369     }
370 
371 
372     protected Point measureAxisXDesc() {
373         Point sz;
374         foreach (ref bar ; _bars) {
375             bar._titleSize = font.measureMultilineText(bar.title, 0, _barWidth, 4, 0, textFlags);
376             if (sz.y < bar._titleSize.y)
377                 sz.y = bar._titleSize.y;
378             if (sz.x < bar._titleSize.x)
379                 sz.x = bar._titleSize.y;
380         }
381         return sz;
382     }
383 
384     protected Point measureAxisYDesc() {
385         int maxDescWidth = _axisYMinDescWidth;
386         double currentMaxValue = _maxY;
387         if (approxEqual(_maxY, 0, 0.0000001, 0.0000001))
388             currentMaxValue = 100;
389 
390         Point sz = font.textSize(to!dstring(currentMaxValue), MAX_WIDTH_UNSPECIFIED, 4, 0, textFlags);
391         if (maxDescWidth<sz.x)
392             maxDescWidth=sz.x;
393         _axisYMaxValueDescWidth = sz.x;
394         sz = font.textSize(to!dstring(currentMaxValue / 2), MAX_WIDTH_UNSPECIFIED, 4, 0, textFlags);
395         if (maxDescWidth<sz.x)
396             maxDescWidth=sz.x;
397         _axisYAvgValueDescWidth = sz.x;
398         return Point(maxDescWidth, sz.y);
399     }
400 
401     protected int barYValueToPixels(int axisInPixels, double barYValue ) {
402         double currentMaxValue = _maxY;
403         if (approxEqual(_maxY, 0, 0.0000001, 0.0000001))
404             currentMaxValue = 100;
405 
406         double pixValue = axisInPixels / currentMaxValue;
407         return cast(int) round(barYValue * pixValue);
408     }
409 
410     override void onDraw(DrawBuf buf) {
411         if (visibility != Visibility.Visible)
412             return;
413         super.onDraw(buf);
414 
415         Rect rc = _pos;
416         applyMargins(rc);
417         applyPadding(rc);
418 
419         auto saver = ClipRectSaver(buf, rc, alpha);
420 
421         FontRef font = font();
422         if (_showTitle)
423             font.drawText(buf, rc.left+ (_measuredWidth - _titleSize.x)/2  , rc.top, _title, textColor, 4, 0, textFlags);
424 
425         // draw axises and
426         int x1 = rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength;
427         int x2 = rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength + _axisY.thickness + _axisX.zeroValueDist + _axisX.lengthFromZeroToArrow + _axisX.arrowSize;
428         int y1 = rc.bottom - _axisX.maxDescriptionSize.y - _axisX.segmentTagLength - _axisX.thickness - _axisY.zeroValueDist - _axisY.lengthFromZeroToArrow - _axisY.arrowSize;
429         int y2 = rc.bottom - _axisX.maxDescriptionSize.y - _axisX.segmentTagLength;
430 
431         buf.fillRect(Rect(x1, y1, x2, y2), chartBackgroundColor);
432 
433         // y axis
434         buf.drawLine(Point(x1 + 1, y1), Point(x1 + 1, y2), chartAxisColor);
435 
436         // x axis
437         buf.drawLine(Point(x1, y2 - 1), Point(x2, y2 - 1), chartAxisColor);
438 
439         // top line - will be optional in the future
440         buf.drawLine(Point(x1, y1), Point(x2, y1), chartAxisColor);
441 
442         // right line - will be optional in the future
443         buf.drawLine(Point(x2, y1), Point(x2, y2), chartAxisColor);
444 
445         // draw bars
446 
447         int firstBarX = x1 + _axisY.thickness + _axisX.zeroValueDist;
448         int firstBarY = y2 - _axisX.thickness - _axisY.zeroValueDist;
449 
450         SimpleTextFormatter fmt;
451         foreach (ref bar ; _bars) {
452             // draw bar
453             buf.fillRect(Rect(firstBarX, firstBarY - barYValueToPixels(_axisY.lengthFromZeroToArrow, bar.y), firstBarX + _barWidth, firstBarY), bar.color);
454 
455             // draw x axis segment under bar
456             buf.drawLine(Point(firstBarX + _barWidth / 2, y2), Point(firstBarX + _barWidth / 2, rc.bottom - _axisX.maxDescriptionSize.y), chartSegmentTagColor);
457 
458             // draw x axis description
459             fmt.format(bar.title, font, 0, _barWidth, 4, 0, textFlags);
460             fmt.draw(buf, firstBarX + (_barWidth - bar._titleSize.x) / 2, rc.bottom -  _axisX.maxDescriptionSize.y + (_axisX.maxDescriptionSize.y - bar._titleSize.y) / 2, font, textColor, Align.HCenter);
461 
462             firstBarX += _barWidth + _barDistance;
463         }
464 
465         // segments on y axis and values (now only max and max/2)
466         double currentMaxValue = _maxY;
467         if (approxEqual(_maxY, 0, 0.0000001, 0.0000001))
468             currentMaxValue = 100;
469 
470         int yZero = rc.bottom - _axisX.maxDescriptionSize.y - _axisX.segmentTagLength - _axisX.thickness - _axisY.zeroValueDist;
471         int yMax = yZero - _axisY.lengthFromZeroToArrow;
472         int yAvg = (yZero + yMax) / 2;
473 
474         buf.drawLine(Point(rc.left + _axisY.maxDescriptionSize.x, yMax), Point(rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength, yMax), chartSegmentTagColor);
475         buf.drawLine(Point(rc.left + _axisY.maxDescriptionSize.x, yAvg), Point(rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength, yAvg), chartSegmentTagColor);
476 
477         font.drawText(buf, rc.left + (_axisY.maxDescriptionSize.x - _axisYMaxValueDescWidth), yMax - _axisY.maxDescriptionSize.y / 2, to!dstring(currentMaxValue), textColor, 4, 0, textFlags);
478         font.drawText(buf, rc.left + (_axisY.maxDescriptionSize.x - _axisYAvgValueDescWidth), yAvg - _axisY.maxDescriptionSize.y / 2, to!dstring(currentMaxValue / 2), textColor, 4, 0, textFlags);
479 
480     }
481 
482     override void onThemeChanged() {
483         super.onThemeChanged();
484         handleFontChanged();
485     }
486 }
487