1 /*
2 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3 *
4 * See the LICENSE file for terms of use.
5 */
6
7 #include <cmath>
8
9 #include "Wt/Chart/WAbstractChartModel.h"
10 #include "Wt/Chart/WAxisSliderWidget.h"
11 #include "Wt/Chart/WChart2DImplementation.h"
12 #include "Wt/Chart/WDataSeries.h"
13 #include "Wt/Chart/WCartesianChart.h"
14 #include "Wt/Chart/WStandardPalette.h"
15
16 #include "Wt/WAbstractArea.h"
17 #include "Wt/WAbstractItemModel.h"
18 #include "Wt/WApplication.h"
19 #include "Wt/WCanvasPaintDevice.h"
20 #include "Wt/WCircleArea.h"
21 #include "Wt/WException.h"
22 #include "Wt/WEnvironment.h"
23 #include "Wt/WJavaScriptHandle.h"
24 #include "Wt/WJavaScriptObjectStorage.h"
25 #include "Wt/WJavaScriptPreamble.h"
26 #include "Wt/WLogger.h"
27 #include "Wt/WMeasurePaintDevice.h"
28 #include "Wt/WPainter.h"
29 #include "Wt/WPolygonArea.h"
30 #include "Wt/WRectArea.h"
31 #include "Wt/WText.h"
32 #include "Wt/WTheme.h"
33
34 #include "DomElement.h"
35 #include "WebUtils.h"
36
37 #ifndef WT_DEBUG_JS
38 #include "js/ChartCommon.min.js"
39
40 #ifndef WT_TARGET_JAVA
41 namespace skeletons {
42 extern std::vector<const char*> WCartesianChart_js();
43 }
44
45 namespace {
46 using namespace Wt;
WCartesianChart_js_str()47 std::string WCartesianChart_js_str()
48 {
49 std::vector<const char *> v = skeletons::WCartesianChart_js();
50 WStringStream ss;
51 for (std::size_t i = 0; i < v.size(); ++i) {
52 ss << std::string(v[i]);
53 }
54 return ss.str();
55 }
56
wtjs1()57 WJavaScriptPreamble wtjs1() {
58 static std::string js = WCartesianChart_js_str();
59 return WJavaScriptPreamble(WtClassScope, JavaScriptConstructor, "WCartesianChart", js.c_str());
60 }
61 }
62 #else
63 #include "js/WCartesianChart.min.js"
64 #endif
65 #endif
66
67 namespace {
68 using namespace Wt::Chart;
locToJsString(AxisValue loc)69 std::string locToJsString(AxisValue loc) {
70 switch (loc) {
71 case AxisValue::Minimum:
72 return "min";
73 case AxisValue::Maximum:
74 return "max";
75 case AxisValue::Zero:
76 return "zero";
77 case AxisValue::Both:
78 return "both";
79 }
80 assert(false);
81 return "";
82 }
83
binarySearchRow(const Wt::Chart::WAbstractChartModel & model,int xColumn,double d,int minRow,int maxRow)84 int binarySearchRow(const Wt::Chart::WAbstractChartModel &model, int xColumn, double d, int minRow, int maxRow)
85 {
86 if (minRow == maxRow)
87 return minRow;
88 double min = model.data(minRow, xColumn);
89 double max = model.data(maxRow, xColumn);
90 if (d <= min)
91 return minRow;
92 if (d >= max)
93 return maxRow;
94 double start = minRow + (d - min) / (max - min) * (maxRow - minRow);
95 double data = model.data(static_cast<int>(start), xColumn);
96 if (data < d) {
97 return binarySearchRow(model, xColumn, d, static_cast<int>(start) + 1, maxRow);
98 } else if (data > d) {
99 return binarySearchRow(model, xColumn, d, minRow, static_cast<int>(start) - 1);
100 } else {
101 return static_cast<int>(start);
102 }
103 }
104 }
105
106 #ifndef M_PI
107 #define M_PI 3.14159265358979323846
108 #endif
109
110 namespace {
111 const int TICK_LENGTH = 5;
112 const int CURVE_LABEL_PADDING = 10;
113 const int DEFAULT_CURVE_LABEL_WIDTH = 100;
114 const int CURVE_SELECTION_DISTANCE_SQUARED = 400; // Maximum selection distance in pixels, squared
115
toZoomLevel(double zoomFactor)116 inline int toZoomLevel(double zoomFactor)
117 {
118 return ((int)(std::floor(std::log(zoomFactor) / std::log(2.0) + 0.5))) + 1;
119 }
120 }
121
122 namespace Wt {
123
124 LOGGER("WCartesianChart");
125
126 namespace Chart {
127
lightenColor(const WColor & in)128 WColor WCartesianChart::lightenColor(const WColor& in)
129 {
130 double hsl[3];
131 in.toHSL(hsl);
132 double h = hsl[0], s = hsl[1], l = hsl[2];
133 return WColor::fromHSL(h, std::max(s - 0.2, 0.0), std::min(l + 0.2, 1.0), in.alpha());
134 }
135
CurveLabel(const WDataSeries & series,const WPointF & point,const WT_USTRING & label)136 CurveLabel::CurveLabel(const WDataSeries &series, const WPointF &point, const WT_USTRING &label)
137 : series_(&series),
138 label_(label),
139 offset_(60, -20),
140 width_(0),
141 linePen_(WColor(0,0,0)),
142 textPen_(WColor(0,0,0)),
143 boxBrush_(WColor(255,255,255)),
144 markerBrush_(WColor(0,0,0))
145 {
146 x_ = point.x();
147 y_ = point.y();
148 }
149
CurveLabel(const WDataSeries & series,const cpp17::any & x,const cpp17::any & y,const WT_USTRING & label)150 CurveLabel::CurveLabel(const WDataSeries &series, const cpp17::any &x, const cpp17::any &y,const WT_USTRING &label)
151 : series_(&series),
152 x_(x),
153 y_(y),
154 label_(label),
155 offset_(60, -20),
156 width_(0),
157 linePen_(WColor(0,0,0)),
158 textPen_(WColor(0,0,0)),
159 boxBrush_(WColor(255,255,255)),
160 markerBrush_(WColor(0,0,0))
161 { }
162
setSeries(const WDataSeries & series)163 void CurveLabel::setSeries(const WDataSeries &series)
164 {
165 series_ = &series;
166 }
167
setPoint(const WPointF & point)168 void CurveLabel::setPoint(const WPointF &point)
169 {
170 x_ = point.x();
171 y_ = point.y();
172 }
173
setPoint(const cpp17::any & x,const cpp17::any & y)174 void CurveLabel::setPoint(const cpp17::any &x, const cpp17::any &y)
175 {
176 x_ = x;
177 y_ = y;
178 }
179
point()180 const WPointF CurveLabel::point() const
181 {
182 return WPointF(asNumber(x_), asNumber(y_));
183 }
184
setLabel(const WT_USTRING & label)185 void CurveLabel::setLabel(const WT_USTRING &label)
186 {
187 label_ = label;
188 }
189
setOffset(const WPointF & offset)190 void CurveLabel::setOffset(const WPointF &offset)
191 {
192 offset_ = offset;
193 }
194
setWidth(int width)195 void CurveLabel::setWidth(int width)
196 {
197 width_ = width;
198 }
199
setLinePen(const WPen & pen)200 void CurveLabel::setLinePen(const WPen &pen)
201 {
202 linePen_ = pen;
203 }
204
setTextPen(const WPen & pen)205 void CurveLabel::setTextPen(const WPen &pen)
206 {
207 textPen_ = pen;
208 }
209
setBoxBrush(const WBrush & brush)210 void CurveLabel::setBoxBrush(const WBrush &brush)
211 {
212 boxBrush_ = brush;
213 }
214
setMarkerBrush(const WBrush & brush)215 void CurveLabel::setMarkerBrush(const WBrush &brush)
216 {
217 markerBrush_ = brush;
218 }
219
220 // Checks if edge [p1,p2] intersects the horizontal edge [(minX,y),(maxX,y)]
checkIntersectHorizontal(const WPointF & p1,const WPointF & p2,double minX,double maxX,double y)221 bool CurveLabel::checkIntersectHorizontal(const WPointF &p1, const WPointF &p2, double minX, double maxX, double y)
222 {
223 if (p1.y() == p2.y()) return p1.y() == y;
224 double t = (y - p1.y()) / (p2.y() - p1.y());
225 if (t <= 0 || t >= 1) return false; // The edge does not reach y, we do not consider the ends
226 double x = p1.x() * (1 - t) + p2.x() * t;
227 return x > minX && x < maxX; // do not consider the ends
228 }
229
230 // Checks if edge [p1,p2] intersects the vertical edge [(x,minY),(x,maxY)]
checkIntersectVertical(const WPointF & p1,const WPointF & p2,double minY,double maxY,double x)231 bool CurveLabel::checkIntersectVertical(const WPointF &p1, const WPointF &p2, double minY, double maxY, double x)
232 {
233 return checkIntersectHorizontal(WPointF(p1.y(), p1.x()), WPointF(p2.y(), p2.x()), minY, maxY, x);
234 }
235
render(WPainter & painter)236 void CurveLabel::render(WPainter &painter) const
237 {
238 WRectF rect;
239 {
240 // Determine rectangle width, width if nonzero, otherwise calculated using font metrics or, if unavailable, DEFAULT_CURVE_LABEL_WIDTH
241 double rectWidth = DEFAULT_CURVE_LABEL_WIDTH;
242 if (width() != 0) {
243 rectWidth = width();
244 } else if (painter.device()->features().test(
245 PaintDeviceFeatureFlag::FontMetrics)) {
246 WMeasurePaintDevice device(painter.device());
247 WPainter measPainter(&device);
248 measPainter.drawText(WRectF(0,0,100,100),
249 Wt::WFlags<AlignmentFlag>(AlignmentFlag::Middle) | AlignmentFlag::Center,
250 TextFlag::SingleLine, label(), nullptr);
251 rectWidth = device.boundingRect().width() + CURVE_LABEL_PADDING / 2;
252 }
253 rect = WRectF(offset().x() - rectWidth / 2, offset().y() - 10,
254 rectWidth, 20).normalized();
255 }
256 WPointF closestAnchor;
257 {
258 // Find the closest point on the side of the rectangle to connect to,
259 // that does not cause the line to it from the starting point to intersect
260 // with an edge of the rectangle.
261 std::vector<WPointF> anchorPoints;
262 anchorPoints.push_back(WPointF(rect.left(), rect.center().y()));
263 anchorPoints.push_back(WPointF(rect.right(), rect.center().y()));
264 anchorPoints.push_back(WPointF(rect.center().x(), rect.top()));
265 anchorPoints.push_back(WPointF(rect.center().x(), rect.bottom()));
266 double minSquareDist = std::numeric_limits<double>::infinity();
267 for (std::size_t k = 0; k < anchorPoints.size(); ++k) {
268 const WPointF &anchorPoint = anchorPoints[k];
269 double d = anchorPoint.x() * anchorPoint.x() + anchorPoint.y() * anchorPoint.y();
270 if (d < minSquareDist &&
271 (k == 0 || !checkIntersectVertical(WPointF(), anchorPoint, rect.top(), rect.bottom(), rect.left())) &&
272 (k == 1 || !checkIntersectVertical(WPointF(), anchorPoint, rect.top(), rect.bottom(), rect.right())) &&
273 (k == 2 || !checkIntersectHorizontal(WPointF(), anchorPoint, rect.left(), rect.right(), rect.top())) &&
274 (k == 3 || !checkIntersectHorizontal(WPointF(), anchorPoint, rect.left(), rect.right(), rect.bottom()))) {
275 closestAnchor = anchorPoint;
276 minSquareDist = d;
277 }
278 }
279 }
280 WTransform translation = painter.worldTransform();
281 painter.setWorldTransform(WTransform());
282 WPainterPath connectorLine;
283 connectorLine.moveTo(0, 0);
284 connectorLine.lineTo(closestAnchor);
285 painter.strokePath(translation.map(connectorLine).crisp(), linePen());
286 WPainterPath circle;
287 circle.addEllipse(-2.5, -2.5, 5, 5);
288 painter.fillPath(translation.map(circle), markerBrush());
289 WPainterPath rectPath;
290 rectPath.addRect(rect);
291 painter.fillPath(translation.map(rectPath), boxBrush());
292 painter.strokePath(translation.map(rectPath).crisp(), linePen());
293 painter.setPen(textPen());
294 painter.drawText(translation.map(rect),
295 WFlags<AlignmentFlag>(AlignmentFlag::Middle) | AlignmentFlag::Center,
296 TextFlag::SingleLine, label(), nullptr);
297 }
298
~SeriesIterator()299 SeriesIterator::~SeriesIterator()
300 { }
301
startSegment(int currentXSegment,int currentYSegment,const WRectF & currentSegmentArea)302 void SeriesIterator::startSegment(int currentXSegment, int currentYSegment,
303 const WRectF& currentSegmentArea)
304 {
305 currentXSegment_ = currentXSegment;
306 currentYSegment_ = currentYSegment;
307 }
308
endSegment()309 void SeriesIterator::endSegment()
310 {
311 }
312
startSeries(const WDataSeries & series,double groupWidth,int numBarGroups,int currentBarGroup)313 bool SeriesIterator::startSeries(const WDataSeries& series, double groupWidth,
314 int numBarGroups, int currentBarGroup)
315 {
316 return true;
317 }
318
endSeries()319 void SeriesIterator::endSeries()
320 { }
321
newValue(const WDataSeries & series,double x,double y,double stackY,int xRow,int xColumn,int yRow,int yColumn)322 void SeriesIterator::newValue(const WDataSeries& series,
323 double x, double y, double stackY,
324 int xRow, int xColumn,
325 int yRow, int yColumn)
326 { }
327
328
setPenColor(WPen & pen,const WDataSeries & series,int xRow,int xColumn,int yRow,int yColumn,ItemDataRole colorRole)329 void SeriesIterator::setPenColor(WPen& pen, const WDataSeries &series,
330 int xRow, int xColumn,
331 int yRow, int yColumn,
332 ItemDataRole colorRole)
333 {
334 const WColor *color = nullptr;
335
336 if (yRow >= 0 && yColumn >= 0) {
337 if (colorRole == ItemDataRole::MarkerPenColor) {
338 color = series.model()->markerPenColor(yRow, yColumn);
339 } else if (colorRole == ItemDataRole::MarkerBrushColor) {
340 color = series.model()->markerBrushColor(yRow, yColumn);
341 }
342 }
343
344 if (!color && xRow >= 0 && xColumn >= 0) {
345 if (colorRole == ItemDataRole::MarkerPenColor) {
346 color = series.model()->markerPenColor(xRow, xColumn);
347 } else if (colorRole == ItemDataRole::MarkerBrushColor) {
348 color = series.model()->markerBrushColor(xRow, xColumn);
349 }
350 }
351
352 if (color)
353 pen.setColor(*color);
354 }
355
setBrushColor(WBrush & brush,const WDataSeries & series,int xRow,int xColumn,int yRow,int yColumn,ItemDataRole colorRole)356 void SeriesIterator::setBrushColor(WBrush& brush, const WDataSeries &series,
357 int xRow, int xColumn,
358 int yRow, int yColumn,
359 ItemDataRole colorRole)
360 {
361 const WColor *color = nullptr;
362
363 if (yRow >= 0 && yColumn >= 0) {
364 if (colorRole == ItemDataRole::MarkerBrushColor) {
365 color = series.model()->markerBrushColor(yRow, yColumn);
366 } else if (colorRole == ItemDataRole::BarBrushColor) {
367 color = series.model()->barBrushColor(yRow, yColumn);
368 }
369 }
370
371 if (!color && xRow >= 0 && xColumn >= 0) {
372 if (colorRole == ItemDataRole::MarkerBrushColor) {
373 color = series.model()->markerBrushColor(xRow, xColumn);
374 } else if (colorRole == ItemDataRole::BarBrushColor) {
375 color = series.model()->barBrushColor(xRow, xColumn);
376 }
377 }
378
379 if (color)
380 brush.setColor(*color);
381 }
382
383 class SeriesRenderer;
384
385 class SeriesRenderIterator final : public SeriesIterator
386 {
387 public:
388 SeriesRenderIterator(const WCartesianChart& chart, WPainter& painter);
389
390 virtual void startSegment(int currentXSegment, int currentYSegment,
391 const WRectF& currentSegmentArea) override;
392 virtual void endSegment() override;
393
394 virtual bool startSeries(const WDataSeries& series, double groupWidth,
395 int numBarGroups, int currentBarGroup) override;
396 virtual void endSeries() override;
397
398 virtual void newValue(const WDataSeries& series, double x, double y,
399 double stackY,
400 int xRow, int xColumn,
401 int yRow, int yColumn) override;
402
403 double breakY(double y);
404
405 private:
406 const WCartesianChart& chart_;
407 WPainter& painter_;
408 const WDataSeries *series_;
409 SeriesRenderer *seriesRenderer_;
410 double minY_, maxY_;
411 };
412
413 class SeriesRenderer {
414 public:
~SeriesRenderer()415 virtual ~SeriesRenderer() { }
416 virtual void addBreak() = 0;
417 virtual void addValue(double x, double y, double stacky,
418 int xRow, int xColumn, int yRow, int yColumn)
419 = 0;
420 virtual void paint() = 0;
421
422 protected:
423 const WCartesianChart& chart_;
424 WPainter& painter_;
425 const WDataSeries& series_;
426 SeriesRenderIterator& it_;
427
SeriesRenderer(const WCartesianChart & chart,WPainter & painter,const WDataSeries & series,SeriesRenderIterator & it)428 SeriesRenderer(const WCartesianChart& chart, WPainter& painter,
429 const WDataSeries& series, SeriesRenderIterator& it)
430 : chart_(chart),
431 painter_(painter),
432 series_(series),
433 it_(it)
434 { }
435
crisp(double u)436 static double crisp(double u) {
437 return std::floor(u) + 0.5;
438 }
439
hv(const WPointF & p)440 WPointF hv(const WPointF& p) {
441 return chart_.hv(p);
442 }
443
hv(double x,double y)444 WPointF hv(double x, double y) {
445 return chart_.hv(x, y);
446 }
447 };
448
449 class LineSeriesRenderer final : public SeriesRenderer {
450 public:
LineSeriesRenderer(const WCartesianChart & chart,WPainter & painter,const WDataSeries & series,SeriesRenderIterator & it)451 LineSeriesRenderer(const WCartesianChart& chart, WPainter& painter,
452 const WDataSeries& series,
453 SeriesRenderIterator& it)
454 : SeriesRenderer(chart, painter, series, it),
455 curveLength_(0),
456 curveFragmentLength_(0)
457 {
458 curve_.setOpenSubPathsEnabled(true);
459 }
460
addValue(double x,double y,double stacky,int xRow,int xColumn,int yRow,int yColumn)461 virtual void addValue(double x, double y, double stacky,
462 int xRow, int xColumn, int yRow, int yColumn) override {
463 WPointF p = chart_.map(x, y, chart_.xAxis(series_.xAxis()), chart_.yAxis(series_.yAxis()),
464 it_.currentXSegment(), it_.currentYSegment());
465
466 if (curveFragmentLength_ == 0) {
467 curve_.moveTo(hv(p));
468
469 if (series_.fillRange() != FillRangeType::None
470 && series_.brush() != BrushStyle::None) {
471 fill_.moveTo(hv(fillOtherPoint(x)));
472 fill_.lineTo(hv(p));
473 }
474 } else {
475 if (series_.type() == SeriesType::Line) {
476 curve_.lineTo(hv(p));
477 fill_.lineTo(hv(p));
478 } else {
479 if (curveFragmentLength_ == 1) {
480 computeC(p0, p, c_);
481 } else {
482 WPointF c1, c2;
483 computeC(p_1, p0, p, c1, c2);
484 curve_.cubicTo(hv(c_), hv(c1), hv(p0));
485 fill_.cubicTo(hv(c_), hv(c1), hv(p0));
486 c_ = c2;
487 }
488 }
489 }
490
491 p_1 = p0;
492 p0 = p;
493 lastX_ = x;
494 ++curveLength_;
495 ++curveFragmentLength_;
496 }
497
addBreak()498 virtual void addBreak() override
499 {
500 if (curveFragmentLength_ > 1) {
501 if (series_.type() == SeriesType::Curve) {
502 WPointF c1;
503 computeC(p0, p_1, c1);
504 curve_.cubicTo(hv(c_), hv(c1), hv(p0));
505 fill_.cubicTo(hv(c_), hv(c1), hv(p0));
506 }
507
508 if (series_.fillRange() != FillRangeType::None
509 && series_.brush() != BrushStyle::None) {
510 fill_.lineTo(hv(fillOtherPoint(lastX_)));
511 fill_.closeSubPath();
512 }
513 }
514 curveFragmentLength_ = 0;
515 }
516
paint()517 virtual void paint() override {
518 WCartesianChart::PainterPathMap::iterator curveHandle =
519 chart_.curvePaths_.find(&series_);
520 WCartesianChart::TransformMap::iterator transformHandle =
521 chart_.curveTransforms_.find(&series_);
522
523 WTransform transform = chart_.zoomRangeTransform(chart_.xAxis(series_.xAxis()),
524 chart_.yAxis(series_.yAxis()));
525
526 if (curveLength_ > 1) {
527 if (series_.type() == SeriesType::Curve) {
528 WPointF c1;
529 computeC(p0, p_1, c1);
530 curve_.cubicTo(hv(c_), hv(c1), hv(p0));
531 fill_.cubicTo(hv(c_), hv(c1), hv(p0));
532 }
533
534 if (series_.fillRange() != FillRangeType::None
535 && series_.brush() != BrushStyle::None && !series_.isHidden()) {
536 fill_.lineTo(hv(fillOtherPoint(lastX_)));
537 fill_.closeSubPath();
538 painter_.setShadow(series_.shadow());
539 WBrush brush = series_.brush();
540 if (chart_.seriesSelectionEnabled() &&
541 chart_.selectedSeries() != nullptr &&
542 chart_.selectedSeries() != &series_) {
543 brush.setColor(WCartesianChart::lightenColor(brush.color()));
544 }
545 painter_.fillPath(transform.map(fill_), brush); // FIXME: support curve transforms
546 }
547
548 if (series_.fillRange() == FillRangeType::None)
549 painter_.setShadow(series_.shadow());
550 else
551 painter_.setShadow(WShadow());
552
553 WTransform ct;
554 WTransform t = chart_.calculateCurveTransform(series_);
555 if (transformHandle != chart_.curveTransforms_.end()) {
556 transformHandle->second.setValue(t);
557 ct = chart_.curveTransform(series_);
558 } else {
559 ct = t;
560 if (chart_.orientation() == Orientation::Horizontal) {
561 ct = WTransform(0,1,1,0,0,0) * ct * WTransform(0,1,1,0,0,0);
562 }
563 }
564 series_.scaleDirty_ = false;
565 series_.offsetDirty_ = false;
566
567 const WPainterPath *curve = nullptr;
568 if (curveHandle != chart_.curvePaths_.end()) {
569 curveHandle->second.setValue(curve_);
570 curve = &curveHandle->second.value();
571 } else {
572 curve = &curve_;
573 }
574 if (!series_.isHidden()) {
575 WPen pen = series_.pen();
576 if (chart_.seriesSelectionEnabled() &&
577 chart_.selectedSeries() != nullptr &&
578 chart_.selectedSeries() != &series_) {
579 pen.setColor(WCartesianChart::lightenColor(pen.color()));
580 }
581 painter_.strokePath((transform * ct).map(*curve), pen);
582 }
583 }
584
585 curveLength_ = 0;
586 curveFragmentLength_ = 0;
587 curve_ = WPainterPath();
588 fill_ = WPainterPath();
589 }
590
591 private:
592 int curveLength_;
593 int curveFragmentLength_;
594 WPainterPath curve_;
595 WPainterPath fill_;
596
597 double lastX_;
598 WPointF p_1, p0, c_;
599
dist(const WPointF & p1,const WPointF & p2)600 static double dist(const WPointF& p1, const WPointF& p2) {
601 double dx = p2.x() - p1.x();
602 double dy = p2.y() - p1.y();
603 return std::sqrt (dx*dx + dy*dy);
604 }
605
computeC(const WPointF & p,const WPointF & p1,WPointF & c)606 static void computeC(const WPointF& p, const WPointF& p1, WPointF& c) {
607 c.setX(p.x() + 0.3 * (p1.x() - p.x()));
608 c.setY(p.y() + 0.3 * (p1.y() - p.y()));
609 }
610
computeC(const WPointF & p_1,const WPointF & p0,const WPointF & p1,WPointF & c1,WPointF & c2)611 static void computeC(const WPointF& p_1, const WPointF& p0,
612 const WPointF& p1,
613 WPointF& c1, WPointF& c2) {
614 double m1x = (p_1.x() + p0.x())/2.0;
615 double m1y = (p_1.y() + p0.y())/2.0;
616
617 double m2x = (p0.x() + p1.x())/2.0;
618 double m2y = (p0.y() + p1.y())/2.0;
619
620 double L1 = dist(p_1, p0);
621 double L2 = dist(p0, p1);
622 double r = L1/(L1 + L2);
623
624 c1.setX(p0.x() - r * (m2x - m1x));
625 c1.setY(p0.y() - r * (m2y - m1y));
626
627 r = 1-r;
628
629 c2.setX(p0.x() - r * (m1x - m2x));
630 c2.setY(p0.y() - r * (m1y - m2y));
631 }
632
fillOtherPoint(double x)633 WPointF fillOtherPoint(double x) const {
634 FillRangeType fr = series_.fillRange();
635
636 switch (fr) {
637 case FillRangeType::MinimumValue:
638 return WPointF(chart_.map(x, 0,
639 chart_.xAxis(series_.xAxis()),
640 chart_.yAxis(series_.yAxis()),
641 it_.currentXSegment(),
642 it_.currentYSegment()).x(),
643 chart_.chartArea_.bottom());
644 case FillRangeType::MaximumValue:
645 return WPointF(chart_.map(x, 0,
646 chart_.xAxis(series_.xAxis()),
647 chart_.yAxis(series_.yAxis()),
648 it_.currentXSegment(),
649 it_.currentYSegment()).x(),
650 chart_.chartArea_.top());
651 case FillRangeType::ZeroValue:
652 return WPointF(chart_.map(x, 0,
653 chart_.xAxis(series_.xAxis()),
654 chart_.yAxis(series_.yAxis()),
655 it_.currentXSegment(),
656 it_.currentYSegment()));
657 default:
658 return WPointF();
659 }
660 }
661 };
662
663 class BarSeriesRenderer final : public SeriesRenderer {
664 public:
BarSeriesRenderer(const WCartesianChart & chart,WPainter & painter,const WDataSeries & series,SeriesRenderIterator & it,double groupWidth,int numGroups,int group)665 BarSeriesRenderer(const WCartesianChart& chart, WPainter& painter,
666 const WDataSeries& series,
667 SeriesRenderIterator& it,
668 double groupWidth, int numGroups, int group)
669 : SeriesRenderer(chart, painter, series, it),
670 groupWidth_(groupWidth),
671 numGroups_(numGroups),
672 group_(group)
673 { }
674
addValue(double x,double y,double stacky,int xRow,int xColumn,int yRow,int yColumn)675 virtual void addValue(double x, double y, double stacky,
676 int xRow, int xColumn, int yRow, int yColumn) override {
677 WPainterPath bar;
678 const WAxis& xAxis = chart_.xAxis(series_.xAxis());
679 const WAxis& yAxis = chart_.yAxis(series_.yAxis());
680
681 WPointF topMid = chart_.map(x, y, xAxis, yAxis,
682 it_.currentXSegment(),
683 it_.currentYSegment());
684 WPointF bottomMid = chart_.map(x, stacky, xAxis, yAxis,
685 it_.currentXSegment(),
686 it_.currentYSegment());
687
688 FillRangeType fr = series_.fillRange();
689 switch (fr) {
690 case FillRangeType::MinimumValue:
691 bottomMid = WPointF(chart_.map(x, stacky, xAxis, yAxis,
692 it_.currentXSegment(),
693 it_.currentYSegment()).x(),
694 chart_.chartArea_.bottom());
695 break;
696 case FillRangeType::MaximumValue:
697 bottomMid = WPointF(chart_.map(x, stacky, xAxis, yAxis,
698 it_.currentXSegment(),
699 it_.currentYSegment()).x(),
700 chart_.chartArea_.top());
701 break;
702 default:
703 break;
704 }
705
706 double g = numGroups_ + (numGroups_ - 1) * chart_.barMargin();
707
708 double width = groupWidth_ / g;
709 double left = topMid.x() - groupWidth_ / 2
710 + group_ * width * (1 + chart_.barMargin());
711
712 bool nonZeroWidth = chart_.isInteractive() || crisp(left) != crisp(left + width);
713
714 bar.moveTo(hv(left, topMid.y()));
715 if (nonZeroWidth) {
716 bar.lineTo(hv(left + width, topMid.y()));
717 bar.lineTo(hv(left + width, bottomMid.y()));
718 }
719 bar.lineTo(hv(left, bottomMid.y()));
720 if (nonZeroWidth) {
721 bar.closeSubPath();
722 }
723
724 painter_.setShadow(series_.shadow());
725
726 WTransform transform = chart_.zoomRangeTransform(xAxis, yAxis);
727
728 if (nonZeroWidth) {
729 WBrush brush = WBrush(series_.brush());
730 SeriesIterator::setBrushColor(brush, series_, xRow, xColumn, yRow, yColumn, ItemDataRole::BarBrushColor);
731 painter_.fillPath(transform.map(bar), brush);
732 }
733
734 painter_.setShadow(WShadow());
735
736 WPen pen = WPen(series_.pen());
737 SeriesIterator::setPenColor(pen, series_, xRow, xColumn, yRow, yColumn, ItemDataRole::BarPenColor);
738 painter_.strokePath(transform.map(bar).crisp(), pen);
739
740 WString toolTip = series_.model()->toolTip(yRow, yColumn);
741 if (!toolTip.empty() && nonZeroWidth) {
742 WTransform t = painter_.worldTransform();
743
744 WPointF tl = t.map(segmentPoint(bar, 0));
745 WPointF tr = t.map(segmentPoint(bar, 1));
746 WPointF br = t.map(segmentPoint(bar, 2));
747 WPointF bl = t.map(segmentPoint(bar, 3));
748
749 if (series_.model()->flags(yRow, yColumn).test(ItemFlag::DeferredToolTip) ||
750 // Force deferred tooltips if XHTML text
751 series_.model()->flags(yRow, yColumn).test(ItemFlag::XHTMLText)) {
752 chart_.hasDeferredToolTips_ = true;
753 WCartesianChart::BarTooltip btt(series_, xRow, xColumn, yRow, yColumn);
754 btt.xs[0] = tl.x();
755 btt.ys[0] = tl.y();
756 btt.xs[1] = tr.x();
757 btt.ys[1] = tr.y();
758 btt.xs[2] = br.x();
759 btt.ys[2] = br.y();
760 btt.xs[3] = bl.x();
761 btt.ys[3] = bl.y();
762 chart_.barTooltips_.push_back(btt);
763 } else {
764 double tlx = 0, tly = 0, brx = 0, bry = 0;
765 bool useRect = false;
766 if (fequal(tl.y(), tr.y())) {
767 tlx = std::min(tl.x(), tr.x());
768 brx = std::max(tl.x(), tr.x());
769 tly = std::min(tl.y(), bl.y());
770 bry = std::max(tl.y(), br.y());
771
772 useRect = true;
773 } else if (fequal(tl.x(), tr.x())) {
774 tlx = std::min(tl.x(), bl.x());
775 brx = std::max(tl.x(), bl.x());
776 tly = std::min(tl.y(), tr.y());
777 bry = std::max(tl.y(), tr.y());
778
779 useRect = true;
780 }
781
782 std::unique_ptr<WAbstractArea> area;
783 if (useRect)
784 area.reset(new WRectArea(tlx, tly, (brx - tlx), (bry - tly)));
785 else {
786 WPolygonArea *poly = new WPolygonArea();
787 poly->addPoint(tl.x(), tl.y());
788 poly->addPoint(tr.x(), tr.y());
789 poly->addPoint(br.x(), br.y());
790 poly->addPoint(bl.x(), bl.y());
791 area.reset(poly);
792 }
793
794 area->setToolTip(toolTip);
795
796 const_cast<WCartesianChart&>(chart_)
797 .addDataPointArea(series_, xRow, xColumn, std::move(area));
798 }
799 }
800
801 double bTopMidY = it_.breakY(topMid.y());
802 double bBottomMidY = it_.breakY(bottomMid.y());
803
804 if (bTopMidY > topMid.y() && bBottomMidY <= bottomMid.y()) {
805 WPainterPath breakPath;
806 breakPath.moveTo(hv(left - 10, bTopMidY + 10));
807 breakPath.lineTo(hv(left + width + 10, bTopMidY + 1));
808 breakPath.lineTo(hv(left + width + 10, bTopMidY - 1));
809 breakPath.lineTo(hv(left - 10, bTopMidY - 1));
810 painter_.setPen(PenStyle::None);
811 painter_.setBrush(chart_.background());
812 painter_.drawPath(transform.map(breakPath).crisp());
813 painter_.setPen(WPen());
814 WPainterPath line;
815 line.moveTo(hv(left - 10, bTopMidY + 10));
816 line.lineTo(hv(left + width + 10, bTopMidY + 1));
817 painter_.drawPath(transform.map(line).crisp());
818 }
819
820 if (bBottomMidY < bottomMid.y() && bTopMidY >= topMid.y()) {
821 WPainterPath breakPath;
822 breakPath.moveTo(hv(left + width + 10, bBottomMidY - 10));
823 breakPath.lineTo(hv(left - 10, bBottomMidY - 1));
824 breakPath.lineTo(hv(left - 10, bBottomMidY + 1));
825 breakPath.lineTo(hv(left + width + 10, bBottomMidY + 1));
826 painter_.setBrush(chart_.background());
827 painter_.setPen(PenStyle::None);
828 painter_.drawPath(transform.map(breakPath).crisp());
829 painter_.setPen(WPen());
830 WPainterPath line;
831 line.moveTo(hv(left - 10, bBottomMidY - 1));
832 line.lineTo(hv(left + width + 10, bBottomMidY - 10));
833 painter_.drawPath(transform.map(line).crisp());
834 }
835 }
836
addBreak()837 virtual void addBreak() override { }
paint()838 virtual void paint() override { }
839
840 private:
segmentPoint(const Wt::WPainterPath & path,int segment)841 static Wt::WPointF segmentPoint(const Wt::WPainterPath& path, int segment)
842 {
843 const Wt::WPainterPath::Segment& s = path.segments()[segment];
844 return Wt::WPointF(s.x(), s.y());
845 }
846
fequal(double d1,double d2)847 static bool fequal(double d1, double d2) {
848 return std::fabs(d1 - d2) < 1E-5;
849 }
850
851 private:
852 double groupWidth_;
853 int numGroups_;
854 int group_;
855 };
856
SeriesRenderIterator(const WCartesianChart & chart,WPainter & painter)857 SeriesRenderIterator::SeriesRenderIterator(const WCartesianChart& chart,
858 WPainter& painter)
859 : chart_(chart),
860 painter_(painter),
861 series_(nullptr)
862 { }
863
startSegment(int currentXSegment,int currentYSegment,const WRectF & currentSegmentArea)864 void SeriesRenderIterator::startSegment(int currentXSegment,
865 int currentYSegment,
866 const WRectF& currentSegmentArea)
867 {
868 SeriesIterator::startSegment(currentXSegment, currentYSegment,
869 currentSegmentArea);
870
871 const WAxis& yAxis = chart_.yAxis(series_->yAxis());
872
873 if (currentYSegment == 0)
874 maxY_ = DBL_MAX;
875 else
876 maxY_ = currentSegmentArea.bottom();
877
878 if (currentYSegment == yAxis.segmentCount() - 1)
879 minY_ = -DBL_MAX;
880 else
881 minY_ = currentSegmentArea.top();
882 }
883
endSegment()884 void SeriesRenderIterator::endSegment()
885 {
886 SeriesIterator::endSegment();
887
888 seriesRenderer_->paint();
889 }
890
startSeries(const WDataSeries & series,double groupWidth,int numBarGroups,int currentBarGroup)891 bool SeriesRenderIterator::startSeries(const WDataSeries& series,
892 double groupWidth,
893 int numBarGroups, int currentBarGroup)
894 {
895 seriesRenderer_ = nullptr;
896
897 switch (series.type()) {
898 case SeriesType::Line:
899 case SeriesType::Curve:
900 seriesRenderer_ = new LineSeriesRenderer(chart_, painter_, series, *this);
901 break;
902 case SeriesType::Bar:
903 seriesRenderer_ = new BarSeriesRenderer(chart_, painter_, series, *this,
904 groupWidth,
905 numBarGroups, currentBarGroup);
906 default:
907 break;
908 }
909
910 series_ = &series;
911
912 if (seriesRenderer_ != nullptr) painter_.save();
913
914 return seriesRenderer_ != nullptr;
915 }
916
endSeries()917 void SeriesRenderIterator::endSeries()
918 {
919 seriesRenderer_->paint();
920 painter_.restore();
921
922 delete seriesRenderer_;
923 series_ = nullptr;
924 }
925
newValue(const WDataSeries & series,double x,double y,double stackY,int xRow,int xColumn,int yRow,int yColumn)926 void SeriesRenderIterator::newValue(const WDataSeries& series,
927 double x, double y,
928 double stackY,
929 int xRow, int xColumn,
930 int yRow, int yColumn)
931 {
932 if (Utils::isNaN(x) || Utils::isNaN(y))
933 seriesRenderer_->addBreak();
934 else
935 seriesRenderer_->addValue(x, y, stackY, xRow, xColumn, yRow, yColumn);
936 }
937
breakY(double y)938 double SeriesRenderIterator::breakY(double y)
939 {
940 if (y < minY_)
941 return minY_;
942 else if (y > maxY_)
943 return maxY_;
944 else
945 return y;
946 }
947
948 class LabelRenderIterator : public SeriesIterator
949 {
950 public:
LabelRenderIterator(const WCartesianChart & chart,WPainter & painter)951 LabelRenderIterator(const WCartesianChart& chart, WPainter& painter)
952 : chart_(chart),
953 painter_(painter)
954 { }
955
startSeries(const WDataSeries & series,double groupWidth,int numBarGroups,int currentBarGroup)956 virtual bool startSeries(const WDataSeries& series, double groupWidth,
957 int numBarGroups, int currentBarGroup) override
958 {
959 if (series.isLabelsEnabled(Axis::X) ||
960 series.isLabelsEnabled(Axis::Y)) {
961 groupWidth_ = groupWidth;
962 numGroups_ = numBarGroups;
963 group_ = currentBarGroup;
964 return true;
965 } else
966 return false;
967 }
968
newValue(const WDataSeries & series,double x,double y,double stackY,int xRow,int xColumn,int yRow,int yColumn)969 virtual void newValue(const WDataSeries& series, double x, double y,
970 double stackY,
971 int xRow, int xColumn,
972 int yRow, int yColumn) override
973 {
974 if (Utils::isNaN(x) || Utils::isNaN(y))
975 return;
976
977 WString text;
978
979 if (series.isLabelsEnabled(Axis::X)) {
980 text = chart_.xAxis(series.xAxis()).label(x);
981 }
982
983 if (series.isLabelsEnabled(Axis::Y)) {
984 if (!text.empty())
985 text += ": ";
986 text += chart_.yAxis(series.yAxis()).label(y - stackY);
987 }
988
989 if (!text.empty()) {
990 WPointF point = chart_.map(x, y, series.axis(),
991 currentXSegment(), currentYSegment());
992 WPointF p = point;
993 if (series.type() == SeriesType::Bar) {
994 double g = numGroups_ + (numGroups_ - 1) * chart_.barMargin();
995
996 double width = groupWidth_ / g;
997 double left = p.x() - groupWidth_ / 2
998 + group_ * width * (1 + chart_.barMargin());
999
1000 p = WPointF(left + width/2, p.y());
1001 }
1002
1003 WFlags<AlignmentFlag> alignment;
1004 if (series.type() == SeriesType::Bar) {
1005 if (y < 0)
1006 alignment = WFlags<AlignmentFlag>(AlignmentFlag::Center) | AlignmentFlag::Bottom;
1007 else
1008 alignment = WFlags<AlignmentFlag>(AlignmentFlag::Center) | AlignmentFlag::Top;
1009 } else {
1010 alignment = WFlags<AlignmentFlag>(AlignmentFlag::Center) | AlignmentFlag::Bottom;
1011 p.setY(p.y() - 3);
1012 }
1013
1014 WCartesianChart &chart = const_cast<WCartesianChart &>(chart_);
1015 WPen oldPen = WPen(chart.textPen_);
1016 chart.textPen_.setColor(series.labelColor());
1017 WTransform t = chart_.zoomRangeTransform(chart_.xAxis(series.xAxis()), chart_.yAxis(series.yAxis()));
1018 WTransform ct;
1019 WCartesianChart::TransformMap::const_iterator transformHandle
1020 = chart_.curveTransforms_.find(&series);
1021 if (transformHandle != chart_.curveTransforms_.end()) {
1022 ct = chart_.curveTransform(series);
1023 }
1024 if (series.type() == SeriesType::Bar) {
1025 chart.renderLabel(painter_, text,
1026 chart_.inverseHv((t * ct).map(chart_.hv(p))),
1027 alignment, 0, 3);
1028 } else {
1029 double dx = p.x() - point.x();
1030 double dy = p.y() - point.y();
1031 chart.renderLabel(painter_, text,
1032 chart_.inverseHv((t * ct).translate(WPointF(dx, dy)).map(chart_.hv(point))),
1033 alignment, 0, 3);
1034 }
1035 chart.textPen_ = oldPen;
1036 }
1037 }
1038
1039 private:
1040 const WCartesianChart& chart_;
1041 WPainter& painter_;
1042
1043 double groupWidth_;
1044 int numGroups_;
1045 int group_;
1046 };
1047
1048 class MarkerRenderIterator final : public SeriesIterator
1049 {
1050 public:
MarkerRenderIterator(const WCartesianChart & chart,WPainter & painter)1051 MarkerRenderIterator(const WCartesianChart& chart, WPainter& painter)
1052 : chart_(chart),
1053 painter_(painter),
1054 currentMarkerType_(MarkerType::None),
1055 currentScale_(0),
1056 series_(0)
1057 { }
1058
startSeries(const WDataSeries & series,double groupWidth,int numBarGroups,int currentBarGroup)1059 virtual bool startSeries(const WDataSeries& series, double groupWidth,
1060 int numBarGroups, int currentBarGroup) override
1061 {
1062 marker_ = WPainterPath();
1063
1064 if (series.marker() != MarkerType::None) {
1065 chart_.drawMarker(series, marker_);
1066 painter_.save();
1067 needRestore_ = true;
1068 } else
1069 needRestore_ = false;
1070
1071 return true;
1072 }
1073
endSeries()1074 virtual void endSeries() override
1075 {
1076 if (series_)
1077 finishPathFragment(*series_);
1078 series_ = 0;
1079
1080 if (needRestore_)
1081 painter_.restore();
1082 }
1083
newValue(const WDataSeries & series,double x,double y,double stackY,int xRow,int xColumn,int yRow,int yColumn)1084 virtual void newValue(const WDataSeries& series, double x, double y,
1085 double stackY,
1086 int xRow, int xColumn,
1087 int yRow, int yColumn) override
1088 {
1089 if (!Utils::isNaN(x) && !Utils::isNaN(y)) {
1090 WPointF p = chart_.map(x, y, chart_.xAxis(series.xAxis()), chart_.yAxis(series.yAxis()),
1091 currentXSegment(), currentYSegment());
1092
1093 const MarkerType *pointMarker = series.model()->markerType(yRow, yColumn);
1094 if (!pointMarker) {
1095 pointMarker = series.model()->markerType(xRow, xColumn);
1096 }
1097 MarkerType markerType = series.marker();
1098 if (pointMarker) {
1099 markerType = *pointMarker;
1100 }
1101 if (markerType != MarkerType::None) {
1102 WPen pen = WPen(series.markerPen());
1103 SeriesIterator::setPenColor(pen, series, xRow, xColumn, yRow, yColumn, ItemDataRole::MarkerPenColor);
1104 if (chart_.seriesSelectionEnabled() &&
1105 chart_.selectedSeries() != nullptr &&
1106 chart_.selectedSeries() != &series) {
1107 pen.setColor(WCartesianChart::lightenColor(pen.color()));
1108 }
1109
1110 WBrush brush = WBrush(series.markerBrush());
1111 SeriesIterator::setBrushColor(brush, series, xRow, xColumn, yRow, yColumn, ItemDataRole::MarkerBrushColor);
1112 double scale = calculateMarkerScale(series, xRow, xColumn, yRow, yColumn, series.markerSize());
1113 if (chart_.seriesSelectionEnabled() &&
1114 chart_.selectedSeries() != nullptr &&
1115 chart_.selectedSeries() != &series) {
1116 brush.setColor(WCartesianChart::lightenColor(brush.color()));
1117 }
1118
1119 if (!series_ ||
1120 brush != currentBrush_ ||
1121 pen != currentPen_ ||
1122 scale != currentScale_ ||
1123 markerType != currentMarkerType_) {
1124 if (series_) {
1125 finishPathFragment(*series_);
1126 }
1127
1128 series_ = &series;
1129 currentBrush_ = brush;
1130 currentPen_ = pen;
1131 currentScale_ = scale;
1132
1133 if (markerType != currentMarkerType_) {
1134 marker_ = WPainterPath();
1135 currentMarkerType_ = markerType;
1136 if (pointMarker) {
1137 chart_.drawMarker(series, markerType, marker_);
1138 } else {
1139 chart_.drawMarker(series, marker_);
1140 }
1141 if (!needRestore_) {
1142 painter_.save();
1143 needRestore_ = true;
1144 }
1145 }
1146 }
1147
1148 pathFragment_.moveTo(hv(p));
1149 }
1150
1151 if (series.type() != SeriesType::Bar) {
1152 WString toolTip = series.model()->toolTip(yRow, yColumn);
1153 if (!toolTip.empty()) {
1154 if (!(series.model()->flags(yRow, yColumn).test(ItemFlag::DeferredToolTip) ||
1155 // We force deferred tooltips if it is XHTML
1156 series.model()->flags(yRow, yColumn).test(ItemFlag::XHTMLText))) {
1157 WTransform t = painter_.worldTransform();
1158
1159 p = t.map(hv(p));
1160
1161 std::unique_ptr<WCircleArea> circleArea(new WCircleArea());
1162 circleArea->setCenter(WPointF(p.x(), p.y()));
1163 const double *scaleFactorP = series.model()
1164 ->markerScaleFactor(yRow, yColumn);
1165 double scaleFactor = scaleFactorP != nullptr ? *scaleFactorP : 1.0;
1166 if (scaleFactor < 1.0)
1167 scaleFactor = 1.0;
1168 circleArea->setRadius(int(scaleFactor * 5.0));
1169 circleArea->setToolTip(toolTip);
1170
1171 const_cast<WCartesianChart&>(chart_)
1172 .addDataPointArea(series, xRow, xColumn, std::move(circleArea));
1173 } else {
1174 chart_.hasDeferredToolTips_ = true;
1175 }
1176 }
1177 }
1178 }
1179 }
1180
hv(const WPointF & p)1181 WPointF hv(const WPointF& p) {
1182 return chart_.hv(p);
1183 }
1184
hv(double x,double y)1185 WPointF hv(double x, double y) {
1186 return chart_.hv(x, y);
1187 }
1188
1189 private:
1190 const WCartesianChart& chart_;
1191 WPainter& painter_;
1192 WPainterPath marker_;
1193 bool needRestore_;
1194
1195 WPainterPath pathFragment_;
1196 WPen currentPen_;
1197 WBrush currentBrush_;
1198 MarkerType currentMarkerType_;
1199 double currentScale_;
1200 const WDataSeries *series_;
1201
calculateMarkerScale(const WDataSeries & series,int xRow,int xColumn,int yRow,int yColumn,double markerSize)1202 double calculateMarkerScale(const WDataSeries &series,
1203 int xRow, int xColumn,
1204 int yRow, int yColumn,
1205 double markerSize)
1206 {
1207 const double *scale = nullptr;
1208 double dScale = 1;
1209 if (yRow >= 0 && yColumn >= 0)
1210 scale = series.model()->markerScaleFactor(yRow, yColumn);
1211
1212 if (!scale && xRow >= 0 && xColumn >= 0)
1213 scale = series.model()->markerScaleFactor(xRow, xColumn);
1214
1215 if (scale)
1216 dScale = *scale;
1217
1218 dScale = markerSize / 6 * dScale;
1219
1220 return dScale;
1221 }
1222
finishPathFragment(const WDataSeries & series)1223 void finishPathFragment(const WDataSeries &series)
1224 {
1225 if (pathFragment_.segments().empty())
1226 return;
1227
1228 painter_.save();
1229
1230 painter_.setWorldTransform(WTransform(currentScale_, 0, 0, currentScale_, 0, 0));
1231
1232 const WAxis &xAxis = chart_.xAxis(series.xAxis());
1233 const WAxis &yAxis = chart_.yAxis(series.yAxis());
1234
1235 WTransform currentTransform = WTransform(1.0 / currentScale_, 0, 0, 1.0 / currentScale_, 0, 0) * chart_.zoomRangeTransform(xAxis, yAxis);
1236
1237 painter_.setPen(PenStyle::None);
1238 painter_.setBrush(BrushStyle::None);
1239 painter_.setShadow(series.shadow());
1240 if (currentMarkerType_ != MarkerType::Cross &&
1241 currentMarkerType_ != MarkerType::XCross &&
1242 currentMarkerType_ != MarkerType::Asterisk &&
1243 currentMarkerType_ != MarkerType::Star) {
1244 painter_.setBrush(currentBrush_);
1245
1246 if (!series.shadow().none())
1247 painter_.drawStencilAlongPath(marker_, currentTransform.map(pathFragment_), false);
1248
1249 painter_.setShadow(WShadow());
1250 }
1251 painter_.setPen(currentPen_);
1252 if (!series.shadow().none())
1253 painter_.setBrush(BrushStyle::None);
1254
1255 painter_.drawStencilAlongPath(marker_, currentTransform.map(pathFragment_), false);
1256
1257 painter_.restore();
1258
1259 pathFragment_ = WPainterPath();
1260 }
1261 };
1262
1263 // Used to find if a given point matches a marker, for tooltips
1264 class MarkerMatchIterator final : public SeriesIterator {
1265 public:
1266 static const double MATCH_RADIUS;
1267
MarkerMatchIterator(const WCartesianChart & chart,std::vector<double> xs,std::vector<double> ys,std::vector<double> rxs,std::vector<double> rys)1268 MarkerMatchIterator(const WCartesianChart &chart,
1269 std::vector<double> xs,
1270 std::vector<double> ys,
1271 std::vector<double> rxs,
1272 std::vector<double> rys)
1273 : chart_(chart),
1274 matchXs_(xs),
1275 rXs_(rxs),
1276 matchYs_(ys),
1277 rYs_(rys),
1278 matchedSeries_(nullptr),
1279 matchedXRow_(-1),
1280 matchedXColumn_(-1),
1281 matchedYRow_(-1),
1282 matchedYColumn_(-1)
1283 { }
1284
startSeries(const WDataSeries & series,double groupWidth,int numBarGroups,int currentBarGroup)1285 bool startSeries(const WDataSeries &series, double groupWidth, int numBarGroups, int currentBarGroup) override
1286 {
1287 return matchedSeries_ == nullptr &&
1288 (series.type() == SeriesType::Point || series.type() == SeriesType::Point || series.type() == SeriesType::Curve);
1289 }
1290
newValue(const WDataSeries & series,double x,double y,double stackY,int xRow,int xColumn,int yRow,int yColumn)1291 void newValue(const WDataSeries &series, double x, double y, double stackY, int xRow, int xColumn, int yRow, int yColumn) override
1292 {
1293 if (matchedSeries_)
1294 return; // we already have a match
1295 if (!Utils::isNaN(x) && !Utils::isNaN(y)) {
1296 const double *scaleFactorP = series.model()->markerScaleFactor(yRow, yColumn);
1297 double scaleFactor = scaleFactorP != nullptr ? *scaleFactorP : 1.0;
1298 if (scaleFactor < 1.0)
1299 scaleFactor = 1.0;
1300 double scaledRx = scaleFactor * rXs_[series.xAxis()];
1301 double scaledRy = scaleFactor * rYs_[series.yAxis()];
1302
1303 WPointF p = chart_.map(x, y, chart_.xAxis(series.xAxis()), chart_.yAxis(series.yAxis()), currentXSegment(), currentYSegment());
1304 double dx = p.x() - matchYs_[series.xAxis()];
1305 double dy = p.y() - matchYs_[series.yAxis()];
1306 double dx2 = dx * dx;
1307 double dy2 = dy * dy;
1308 double rx2 = scaledRx * scaledRx;
1309 double ry2 = scaledRy * scaledRy;
1310 if (dx2/rx2 + dy2/ry2 <= 1) {
1311 matchedXRow_ = xRow;
1312 matchedXColumn_ = xColumn;
1313 matchedYRow_ = yRow;
1314 matchedYColumn_ = yColumn;
1315 matchedSeries_ = &series;
1316 }
1317 }
1318 }
1319
matchedSeries()1320 const WDataSeries *matchedSeries() const { return matchedSeries_; }
xRow()1321 int xRow() const { return matchedXRow_; }
xColumn()1322 int xColumn() const { return matchedXColumn_; }
yRow()1323 int yRow() const { return matchedYRow_; }
yColumn()1324 int yColumn() const { return matchedYColumn_; }
1325
1326 private:
1327 const WCartesianChart &chart_;
1328 std::vector<double> matchXs_;
1329 std::vector<double> rXs_;
1330 std::vector<double> matchYs_;
1331 std::vector<double> rYs_;
1332 const WDataSeries *matchedSeries_;
1333 int matchedXRow_, matchedXColumn_, matchedYRow_, matchedYColumn_;
1334 };
1335
1336 const double MarkerMatchIterator::MATCH_RADIUS = 5;
1337
AxisStruct()1338 WCartesianChart::AxisStruct::AxisStruct() noexcept
1339 : axis(std::make_unique<WAxis>()),
1340 calculatedWidth(0)
1341 { }
1342
AxisStruct(std::unique_ptr<WAxis> ax)1343 WCartesianChart::AxisStruct::AxisStruct(std::unique_ptr<WAxis> ax) noexcept
1344 : axis(std::move(ax)),
1345 calculatedWidth(0)
1346 { }
1347
AxisStruct(AxisStruct && other)1348 WCartesianChart::AxisStruct::AxisStruct(AxisStruct &&other) noexcept
1349 : axis(std::move(other.axis)),
1350 calculatedWidth(other.calculatedWidth),
1351 location(other.location),
1352 transform(other.transform),
1353 transformHandle(std::move(other.transformHandle)),
1354 transformChanged(std::move(other.transformChanged)),
1355 pens(std::move(other.pens))
1356 {
1357 other.calculatedWidth = 0;
1358 other.location = AxisLocation();
1359 other.transform = WTransform();
1360 }
1361
1362 WCartesianChart::AxisStruct &WCartesianChart::AxisStruct::operator=(AxisStruct &&other) noexcept
1363 {
1364 if (this == &other)
1365 return *this;
1366
1367 axis = std::move(other.axis);
1368 calculatedWidth = other.calculatedWidth;
1369 location = other.location;
1370 transform = other.transform;
1371 transformHandle = std::move(other.transformHandle);
1372 transformChanged = std::move(other.transformChanged);
1373 pens = std::move(other.pens);
1374
1375 other.calculatedWidth = 0;
1376 other.location = AxisLocation();
1377 other.transform = WTransform();
1378
1379 return *this;
1380 }
1381
~AxisStruct()1382 WCartesianChart::AxisStruct::~AxisStruct()
1383 { }
1384
WCartesianChart()1385 WCartesianChart::WCartesianChart()
1386 : interface_(new WChart2DImplementation(this)),
1387 orientation_(Orientation::Vertical),
1388 XSeriesColumn_(-1),
1389 type_(ChartType::Category),
1390 barMargin_(0),
1391 axisPadding_(5),
1392 borderPen_(PenStyle::None),
1393 hasDeferredToolTips_(false),
1394 jsDefined_(false),
1395 zoomEnabled_(false),
1396 panEnabled_(false),
1397 rubberBandEnabled_(true),
1398 crosshairEnabled_(false),
1399 crosshairColor_(StandardColor::Black),
1400 crosshairXAxis_(0),
1401 crosshairYAxis_(0),
1402 seriesSelectionEnabled_(false),
1403 selectedSeries_(nullptr),
1404 followCurve_(nullptr),
1405 curveManipulationEnabled_(false),
1406 onDemandLoadingEnabled_(false),
1407 loadingBackground_(StandardColor::LightGray),
1408 cObjCreated_(false),
1409 jsSeriesSelected_(this, "seriesSelected"),
1410 loadTooltip_(this, "loadTooltip")
1411 {
1412 init();
1413 }
1414
WCartesianChart(ChartType type)1415 WCartesianChart::WCartesianChart(ChartType type)
1416 : interface_(new WChart2DImplementation(this)),
1417 orientation_(Orientation::Vertical),
1418 XSeriesColumn_(-1),
1419 type_(type),
1420 barMargin_(0),
1421 axisPadding_(5),
1422 borderPen_(PenStyle::None),
1423 hasDeferredToolTips_(false),
1424 jsDefined_(false),
1425 zoomEnabled_(false),
1426 panEnabled_(false),
1427 rubberBandEnabled_(true),
1428 crosshairEnabled_(false),
1429 crosshairColor_(StandardColor::Black),
1430 crosshairXAxis_(0),
1431 crosshairYAxis_(0),
1432 seriesSelectionEnabled_(false),
1433 selectedSeries_(nullptr),
1434 followCurve_(nullptr),
1435 curveManipulationEnabled_(false),
1436 onDemandLoadingEnabled_(false),
1437 loadingBackground_(StandardColor::LightGray),
1438 cObjCreated_(false),
1439 jsSeriesSelected_(this, "seriesSelected"),
1440 loadTooltip_(this, "loadTooltip")
1441 {
1442 init();
1443 }
1444
~WCartesianChart()1445 WCartesianChart::~WCartesianChart()
1446 {
1447 std::vector<WAxisSliderWidget *> copy
1448 = std::vector<WAxisSliderWidget *>(axisSliderWidgets_);
1449 axisSliderWidgets_.clear();
1450
1451 for (std::size_t i = 0; i < copy.size(); ++i) {
1452 copy[i]->setSeries(nullptr);
1453 }
1454 }
1455
init()1456 void WCartesianChart::init()
1457 {
1458 setPalette(std::make_shared<WStandardPalette>(PaletteFlavour::Muted));
1459
1460 xAxes_.push_back(AxisStruct());
1461 yAxes_.push_back(AxisStruct());
1462 yAxes_.push_back(AxisStruct());
1463 yAxes_.back().axis->setLocation(AxisValue::Maximum);
1464
1465 axis(Axis::X).init(interface_.get(), Axis::X);
1466 axis(Axis::Y1).init(interface_.get(), Axis::Y1);
1467 axis(Axis::Y2).init(interface_.get(), Axis::Y2);
1468 axis(Axis::Y2).setVisible(false);
1469
1470 axis(Axis::X).setPadding(axisPadding_);
1471 axis(Axis::Y1).setPadding(axisPadding_);
1472 axis(Axis::Y2).setPadding(axisPadding_);
1473
1474 axis(Axis::X).setSoftLabelClipping(true);
1475 axis(Axis::Y1).setSoftLabelClipping(true);
1476 axis(Axis::Y2).setSoftLabelClipping(true);
1477
1478 setPlotAreaPadding(40, WFlags<Side>(Side::Left) | Side::Right);
1479 setPlotAreaPadding(30, WFlags<Side>(Side::Top) | Side::Bottom);
1480
1481 xAxes_[0].transformHandle = createJSTransform();
1482 xAxes_[0].transformChanged.reset(new JSignal<>(this, "xTransformChanged0"));
1483
1484 for (int i = 0; i < 2; ++i) {
1485 yAxes_[i].transformHandle = createJSTransform();
1486 yAxes_[i].transformChanged.reset(new JSignal<>(this, "yTransformChanged" + std::to_string(i)));
1487 }
1488
1489 if (WApplication::instance() && WApplication::instance()->environment().ajax()) {
1490 mouseWentDown().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.mouseDown(o, e);}}");
1491 mouseWentUp().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.mouseUp(o, e);}}");
1492 mouseDragged().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.mouseDrag(o, e);}}");
1493 mouseMoved().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.mouseMove(o, e);}}");
1494 mouseWheel().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.mouseWheel(o, e);}}");
1495 mouseWentOut().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.mouseOut(o, e);}}");
1496 touchStarted().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.touchStart(o, e);}}");
1497 touchEnded().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.touchEnd(o, e);}}");
1498 touchMoved().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.touchMoved(o, e);}}");
1499 clicked().connect("function(o, e){var o=" + this->cObjJsRef() + ";if(o){o.clicked(o, e);}}");
1500 jsSeriesSelected_.connect(this, &WCartesianChart::jsSeriesSelected);
1501 loadTooltip_.connect(this, &WCartesianChart::loadTooltip);
1502 voidEventSignal("dragstart", true)->preventDefaultAction(true);
1503 }
1504
1505 wheelActions_[KeyboardModifier::None] = InteractiveAction::PanMatching;
1506 wheelActions_[WFlags<KeyboardModifier>(KeyboardModifier::Alt) |
1507 KeyboardModifier::Control] = InteractiveAction::ZoomX;
1508 wheelActions_[WFlags<KeyboardModifier>(KeyboardModifier::Control) |
1509 KeyboardModifier::Shift] = InteractiveAction::ZoomY;
1510 wheelActions_[KeyboardModifier::Control] = InteractiveAction::ZoomXY;
1511 wheelActions_[WFlags<KeyboardModifier>(KeyboardModifier::Alt) |
1512 KeyboardModifier::Control |
1513 KeyboardModifier::Shift] = InteractiveAction::ZoomXY;
1514 }
1515
setOrientation(Orientation orientation)1516 void WCartesianChart::setOrientation(Orientation orientation)
1517 {
1518 if (orientation_ != orientation) {
1519 orientation_ = orientation;
1520 update();
1521 }
1522 }
1523
setXSeriesColumn(int modelColumn)1524 void WCartesianChart::setXSeriesColumn(int modelColumn)
1525 {
1526 if (XSeriesColumn_ != modelColumn) {
1527 XSeriesColumn_ = modelColumn;
1528 update();
1529 }
1530 }
1531
setType(ChartType type)1532 void WCartesianChart::setType(ChartType type)
1533 {
1534 if (type_ != type) {
1535 type_ = type;
1536 xAxes_[0].axis->init(interface_.get(), Axis::X);
1537 update();
1538 }
1539 }
1540
setTextPen(const WPen & pen)1541 void WCartesianChart::setTextPen(const WPen& pen)
1542 {
1543 if(pen == textPen_)
1544 return;
1545
1546 textPen_ = pen;
1547
1548 for (std::size_t i = 0; i < xAxes_.size(); ++i)
1549 xAxes_[i].axis->setTextPen(pen);
1550 for (std::size_t i = 0; i < yAxes_.size(); ++i)
1551 yAxes_[i].axis->setTextPen(pen);
1552 }
1553
addSeries(std::unique_ptr<WDataSeries> series)1554 void WCartesianChart::addSeries(std::unique_ptr<WDataSeries> series)
1555 {
1556 WDataSeries *s = series.get();
1557
1558 series_.push_back(std::move(series));
1559 s->setChart(this);
1560
1561 if (s->type() == SeriesType::Line ||
1562 s->type() == SeriesType::Curve) {
1563 assignJSPathsForSeries(*s);
1564 assignJSTransformsForSeries(*s);
1565 }
1566
1567 update();
1568 }
1569
assignJSHandlesForAllSeries()1570 void WCartesianChart::assignJSHandlesForAllSeries()
1571 {
1572 if (!isInteractive()) return;
1573 for (std::size_t i = 0; i < series_.size(); ++i) {
1574 const WDataSeries &s = *series_[i];
1575 if (s.type() == SeriesType::Line ||
1576 s.type() == SeriesType::Curve) {
1577 assignJSPathsForSeries(s);
1578 assignJSTransformsForSeries(s);
1579 }
1580 }
1581 }
1582
freeJSHandlesForAllSeries()1583 void WCartesianChart::freeJSHandlesForAllSeries()
1584 {
1585 freeAllJSPaths();
1586 freeAllJSTransforms();
1587 }
1588
assignJSPathsForSeries(const WDataSeries & series)1589 void WCartesianChart::assignJSPathsForSeries(const WDataSeries& series)
1590 {
1591 WJavaScriptHandle<WPainterPath> handle;
1592 if (freePainterPaths_.size() > 0) {
1593 handle = freePainterPaths_.back();
1594 freePainterPaths_.pop_back();
1595 } else {
1596 handle = createJSPainterPath();
1597 }
1598 curvePaths_[&series] = handle;
1599 }
1600
assignJSTransformsForSeries(const WDataSeries & series)1601 void WCartesianChart::assignJSTransformsForSeries(const WDataSeries &series)
1602 {
1603 WJavaScriptHandle<WTransform> handle;
1604 if (freeTransforms_.size() > 0) {
1605 handle = freeTransforms_.back();
1606 freeTransforms_.pop_back();
1607 } else {
1608 handle = createJSTransform();
1609 }
1610 curveTransforms_[&series] = handle;
1611 }
1612
removeSeries(WDataSeries * series)1613 std::unique_ptr<WDataSeries> WCartesianChart::removeSeries(WDataSeries *series)
1614 {
1615 int index = seriesIndexOf(*series);
1616
1617 if (index != -1) {
1618 for (std::size_t i = 0; i < axisSliderWidgets_.size(); ++i) {
1619 if (axisSliderWidgets_[i]->series() == series) {
1620 axisSliderWidgets_[i]->setSeries(nullptr);
1621 }
1622 }
1623 if (series->type() == SeriesType::Line ||
1624 series->type() == SeriesType::Curve) {
1625 freeJSPathsForSeries(*series);
1626 freeJSTransformsForSeries(*series);
1627 }
1628
1629 auto result = std::move(series_[index]);
1630 series_.erase(series_.begin() + index);
1631 update();
1632 return result;
1633 } else
1634 return nullptr;
1635 }
1636
freeJSPathsForSeries(const WDataSeries & series)1637 void WCartesianChart::freeJSPathsForSeries(const WDataSeries &series)
1638 {
1639 freePainterPaths_.push_back(curvePaths_[&series]);
1640 curvePaths_.erase(&series);
1641 }
1642
freeJSTransformsForSeries(const WDataSeries & series)1643 void WCartesianChart::freeJSTransformsForSeries(const WDataSeries &series)
1644 {
1645 freeTransforms_.push_back(curveTransforms_[&series]);
1646 curveTransforms_.erase(&series);
1647 }
1648
seriesIndexOf(int modelColumn)1649 int WCartesianChart::seriesIndexOf(int modelColumn) const
1650 {
1651 for (unsigned i = 0; i < series_.size(); ++i)
1652 if (series_[i]->modelColumn() == modelColumn)
1653 return i;
1654
1655 return -1;
1656 }
1657
seriesIndexOf(const WDataSeries & series)1658 int WCartesianChart::seriesIndexOf(const WDataSeries &series) const
1659 {
1660 for (unsigned i = 0; i < series_.size(); ++i)
1661 if (series_[i].get() == &series)
1662 return i;
1663
1664 return -1;
1665 }
1666
series(int modelColumn)1667 WDataSeries& WCartesianChart::series(int modelColumn)
1668 {
1669 int index = seriesIndexOf(modelColumn);
1670
1671 if (index != -1)
1672 return *series_[index];
1673
1674 throw WException("Column " + std::to_string(modelColumn)
1675 + " not in plot");
1676 }
1677
series(int modelColumn)1678 const WDataSeries& WCartesianChart::series(int modelColumn) const
1679 {
1680 int index = seriesIndexOf(modelColumn);
1681
1682 if (index != -1)
1683 return *series_[index];
1684
1685 throw WException("Column " + std::to_string(modelColumn)
1686 + " not in plot");
1687 }
1688
1689 void WCartesianChart
setSeries(std::vector<std::unique_ptr<WDataSeries>> series)1690 ::setSeries(std::vector<std::unique_ptr<WDataSeries> > series)
1691 {
1692 series_ = std::move(series);
1693
1694 freeJSHandlesForAllSeries();
1695 assignJSHandlesForAllSeries();
1696
1697 for (unsigned i = 0; i < series_.size(); ++i)
1698 series_[i]->setChart(this);
1699
1700 update();
1701 }
1702
freeAllJSPaths()1703 void WCartesianChart::freeAllJSPaths()
1704 {
1705 for (PainterPathMap::const_iterator it = curvePaths_.begin();
1706 it != curvePaths_.end(); ++it) {
1707 freePainterPaths_.push_back(it->second);
1708 }
1709 curvePaths_.clear();
1710 }
1711
freeAllJSTransforms()1712 void WCartesianChart::freeAllJSTransforms()
1713 {
1714 for (TransformMap::const_iterator it = curveTransforms_.begin();
1715 it != curveTransforms_.end(); ++it) {
1716 freeTransforms_.push_back(it->second);
1717 }
1718 curveTransforms_.clear();
1719 }
1720
axis(Axis axis)1721 WAxis& WCartesianChart::axis(Axis axis)
1722 {
1723 if (axis == Axis::X)
1724 return xAxis(0);
1725 else
1726 return yAxis(axis == Axis::Y1 ? 0 : 1);
1727 }
1728
axis(Axis axis)1729 const WAxis& WCartesianChart::axis(Axis axis) const
1730 {
1731 if (axis == Axis::X)
1732 return xAxis(0);
1733 else
1734 return yAxis(axis == Axis::Y1 ? 0 : 1);
1735 }
1736
setAxis(std::unique_ptr<WAxis> waxis,Axis axis)1737 void WCartesianChart::setAxis(std::unique_ptr<WAxis> waxis, Axis axis)
1738 {
1739 if (axis == Axis::X) {
1740 xAxes_[0].axis = std::move(waxis);
1741 xAxes_[0].axis->init(interface_.get(), axis);
1742 } else {
1743 int yIndex = axis == Axis::Y1 ? 0 : 1;
1744 yAxes_[yIndex].axis = std::move(waxis);
1745 yAxes_[yIndex].axis->init(interface_.get(), axis);
1746 }
1747 }
1748
xAxes()1749 std::vector<WAxis*> WCartesianChart::xAxes()
1750 {
1751 std::vector<WAxis*> result;
1752 result.reserve(xAxes_.size());
1753 for (std::size_t i = 0; i < xAxes_.size(); ++i)
1754 result.push_back(xAxes_[i].axis.get());
1755 return result;
1756 }
1757
xAxes()1758 std::vector<const WAxis*> WCartesianChart::xAxes() const
1759 {
1760 std::vector<const WAxis*> result;
1761 result.reserve(xAxes_.size());
1762 for (std::size_t i = 0; i < xAxes_.size(); ++i)
1763 result.push_back(xAxes_[i].axis.get());
1764 return result;
1765 }
1766
yAxes()1767 std::vector<WAxis*> WCartesianChart::yAxes()
1768 {
1769 std::vector<WAxis*> result;
1770 result.reserve(yAxes_.size());
1771 for (std::size_t i = 0; i < yAxes_.size(); ++i)
1772 result.push_back(yAxes_[i].axis.get());
1773 return result;
1774 }
1775
yAxes()1776 std::vector<const WAxis*> WCartesianChart::yAxes() const
1777 {
1778 std::vector<const WAxis*> result;
1779 result.reserve(yAxes_.size());
1780 for (std::size_t i = 0; i < yAxes_.size(); ++i)
1781 result.push_back(yAxes_[i].axis.get());
1782 return result;
1783 }
1784
xAxisCount()1785 int WCartesianChart::xAxisCount() const
1786 {
1787 return static_cast<int>(xAxes_.size());
1788 }
1789
yAxisCount()1790 int WCartesianChart::yAxisCount() const
1791 {
1792 return static_cast<int>(yAxes_.size());
1793 }
1794
xAxis(int i)1795 WAxis &WCartesianChart::xAxis(int i)
1796 {
1797 return *xAxes_[i].axis;
1798 }
1799
xAxis(int i)1800 const WAxis &WCartesianChart::xAxis(int i) const
1801 {
1802 return *xAxes_[i].axis;
1803 }
1804
yAxis(int i)1805 WAxis &WCartesianChart::yAxis(int i)
1806 {
1807 return *yAxes_[i].axis;
1808 }
1809
yAxis(int i)1810 const WAxis &WCartesianChart::yAxis(int i) const
1811 {
1812 return *yAxes_[i].axis;
1813 }
1814
addXAxis(std::unique_ptr<WAxis> waxis)1815 int WCartesianChart::addXAxis(std::unique_ptr<WAxis> waxis)
1816 {
1817 int idx = static_cast<int>(xAxes_.size());
1818 xAxes_.push_back(AxisStruct(std::move(waxis)));
1819 xAxes_[idx].axis->initXAxis(interface_.get(), idx);
1820 xAxes_[idx].axis->setPadding(axisPadding());
1821 xAxes_[idx].axis->setSoftLabelClipping(true);
1822
1823 xAxes_[idx].transformHandle = createJSTransform();
1824 xAxes_[idx].transformChanged.reset(
1825 new JSignal<>(this, "xTransformChanged" + std::to_string(idx)));
1826
1827 update();
1828
1829 return idx;
1830 }
1831
addYAxis(std::unique_ptr<WAxis> waxis)1832 int WCartesianChart::addYAxis(std::unique_ptr<WAxis> waxis)
1833 {
1834 int idx = static_cast<int>(yAxes_.size());
1835 yAxes_.push_back(AxisStruct(std::move(waxis)));
1836 yAxes_[idx].axis->initYAxis(interface_.get(), idx);
1837 yAxes_[idx].axis->setPadding(axisPadding());
1838 yAxes_[idx].axis->setSoftLabelClipping(true);
1839
1840 yAxes_[idx].transformHandle = createJSTransform();
1841 yAxes_[idx].transformChanged.reset(
1842 new JSignal<>(this, "yTransformChanged" + std::to_string(idx)));
1843
1844 update();
1845
1846 return idx;
1847 }
1848
removeXAxis(int xAxisId)1849 std::unique_ptr<WAxis> WCartesianChart::removeXAxis(int xAxisId)
1850 {
1851 {
1852 std::size_t i = 0;
1853 while (i < series_.size()) {
1854 if (series_[i]->xAxis() == xAxisId) {
1855 removeSeries(series_[i].get());
1856 } else {
1857 if (series_[i]->xAxis() > xAxisId) {
1858 series_[i]->bindToXAxis(series_[i]->xAxis() - 1);
1859 }
1860 ++i;
1861 }
1862 }
1863 }
1864 if (crosshairXAxis() > xAxisId) {
1865 setCrosshairXAxis(crosshairXAxis() - 1);
1866 }
1867 clearPensForAxis(Axis::X, xAxisId);
1868 auto result = std::move(xAxes_[xAxisId].axis);
1869 xAxes_.erase(xAxes_.begin() + xAxisId);
1870 for (std::size_t i = 0; i < xAxes_.size(); ++i) {
1871 xAxes_[i].axis->xAxis_ = static_cast<int>(i);
1872 }
1873
1874 update();
1875
1876 return result;
1877 }
1878
removeYAxis(int yAxisId)1879 std::unique_ptr<WAxis> WCartesianChart::removeYAxis(int yAxisId)
1880 {
1881 {
1882 std::size_t i = 0;
1883 while (i < series_.size()) {
1884 if (series_[i]->yAxis() == yAxisId) {
1885 removeSeries(series_[i].get());
1886 } else {
1887 if (series_[i]->yAxis() > yAxisId) {
1888 series_[i]->bindToYAxis(series_[i]->yAxis() - 1);
1889 }
1890 ++i;
1891 }
1892 }
1893 }
1894 if (crosshairYAxis() > yAxisId) {
1895 setCrosshairYAxis(crosshairYAxis() - 1);
1896 }
1897 clearPensForAxis(Axis::Y, yAxisId);
1898 auto result = std::move(yAxes_[yAxisId].axis);
1899 yAxes_.erase(yAxes_.begin() + yAxisId);
1900 for (std::size_t i = 0; i < yAxes_.size(); ++i) {
1901 yAxes_[i].axis->yAxis_ = static_cast<int>(i);
1902 yAxes_[i].axis->axis_ = i == 1 ? Axis::Y2 : Axis::Y;
1903 }
1904
1905 update();
1906
1907 return result;
1908 }
1909
clearXAxes()1910 void WCartesianChart::clearXAxes()
1911 {
1912 while (!series_.empty())
1913 removeSeries(series_[series_.size() - 1].get());
1914 clearPens();
1915 xAxes_.clear();
1916
1917 update();
1918 }
1919
clearYAxes()1920 void WCartesianChart::clearYAxes()
1921 {
1922 while (!series_.empty())
1923 removeSeries(series_[series_.size() - 1].get());
1924 clearPens();
1925 yAxes_.clear();
1926
1927 update();
1928 }
1929
setBarMargin(double margin)1930 void WCartesianChart::setBarMargin(double margin)
1931 {
1932 if (barMargin_ != margin) {
1933 barMargin_ = margin;
1934
1935 update();
1936 }
1937 }
1938
setLegendEnabled(bool enabled)1939 void WCartesianChart::setLegendEnabled(bool enabled)
1940 {
1941 legend_.setLegendEnabled(enabled);
1942
1943 update();
1944 }
1945
setLegendLocation(LegendLocation location,Side side,AlignmentFlag alignment)1946 void WCartesianChart::setLegendLocation(LegendLocation location,
1947 Side side,
1948 AlignmentFlag alignment)
1949 {
1950 legend_.setLegendLocation(location, side, alignment);
1951
1952 update();
1953 }
1954
setLegendColumns(int columns,const WLength & columnWidth)1955 void WCartesianChart::setLegendColumns(int columns, const WLength& columnWidth)
1956 {
1957 legend_.setLegendColumns(columns);
1958 legend_.setLegendColumnWidth(columnWidth);
1959
1960 update();
1961 }
1962
setLegendStyle(const WFont & font,const WPen & border,const WBrush & background)1963 void WCartesianChart::setLegendStyle(const WFont& font,
1964 const WPen& border,
1965 const WBrush& background)
1966 {
1967 legend_.setLegendStyle(font, border, background);
1968
1969 update();
1970 }
1971
createLegendItemWidget(int index)1972 std::unique_ptr<WWidget> WCartesianChart::createLegendItemWidget(int index)
1973 {
1974 std::unique_ptr<WContainerWidget> legendItem(new WContainerWidget());
1975
1976 legendItem->addWidget(std::unique_ptr<WWidget>(new IconWidget(this, index)));
1977 std::unique_ptr<WText> label(new WText(model()->headerData(index)));
1978 label->setVerticalAlignment(AlignmentFlag::Top);
1979 legendItem->addWidget(std::move(label));
1980
1981 return std::move(legendItem);
1982 }
1983
addDataPointArea(const WDataSeries & series,int xRow,int xColumn,std::unique_ptr<WAbstractArea> area)1984 void WCartesianChart::addDataPointArea(const WDataSeries& series,
1985 int xRow, int xColumn,
1986 std::unique_ptr<WAbstractArea> area)
1987 {
1988 if (areas().empty())
1989 addAreaMask();
1990 addArea(std::move(area));
1991 }
1992
mapFromDevice(const WPointF & point,Axis ordinateAxis)1993 WPointF WCartesianChart::mapFromDevice(const WPointF &point, Axis ordinateAxis) const
1994 {
1995 return mapFromDevice(point, ordinateAxis == Axis::Y1 ? 0 : 1);
1996 }
1997
mapFromDevice(const WPointF & point,int ordinateAxis)1998 WPointF WCartesianChart::mapFromDevice(const WPointF &point, int ordinateAxis) const
1999 {
2000 return mapFromDevice(point, xAxis(0), yAxis(ordinateAxis));
2001 }
2002
mapFromDevice(const WPointF & point,const WAxis & xAxis,const WAxis & yAxis)2003 WPointF WCartesianChart::mapFromDevice(const WPointF &point, const WAxis &xAxis, const WAxis &yAxis) const
2004 {
2005 if (isInteractive()) {
2006 return mapFromDeviceWithoutTransform(
2007 zoomRangeTransform(xAxes_[xAxis.xAxis_].transformHandle.value(),
2008 yAxes_[yAxis.yAxis_].transformHandle.value())
2009 .inverted()
2010 .map(point),
2011 xAxis, yAxis);
2012 } else {
2013 return mapFromDeviceWithoutTransform(point, xAxis, yAxis);
2014 }
2015 }
2016
mapFromDeviceWithoutTransform(const WPointF & point,Axis ordinateAxis)2017 WPointF WCartesianChart::mapFromDeviceWithoutTransform(const WPointF& point, Axis ordinateAxis)
2018 const
2019 {
2020 return mapFromDeviceWithoutTransform(point, ordinateAxis == Axis::Y1 ? 0 : 1);
2021 }
2022
mapFromDeviceWithoutTransform(const WPointF & point,int ordinateAxis)2023 WPointF WCartesianChart::mapFromDeviceWithoutTransform(const WPointF& point, int ordinateAxis)
2024 const
2025 {
2026 return mapFromDeviceWithoutTransform(point, xAxis(0), yAxis(ordinateAxis));
2027 }
2028
mapFromDeviceWithoutTransform(const WPointF & point,const WAxis & xAxis,const WAxis & yAxis)2029 WPointF WCartesianChart::mapFromDeviceWithoutTransform(const WPointF &point, const WAxis &xAxis, const WAxis &yAxis) const
2030 {
2031 WPointF p = inverseHv(point.x(), point.y(), width().toPixels());
2032
2033 return WPointF(xAxis.mapFromDevice(p.x() - chartArea_.left()),
2034 yAxis.mapFromDevice(chartArea_.bottom() - p.y()));
2035 }
2036
mapToDevice(const cpp17::any & xValue,const cpp17::any & yValue,Axis axis,int xSegment,int ySegment)2037 WPointF WCartesianChart::mapToDevice(const cpp17::any &xValue,
2038 const cpp17::any &yValue,
2039 Axis axis, int xSegment, int ySegment) const
2040 {
2041 return mapToDevice(xValue, yValue, axis == Axis::Y1 ? 0 : 1, xSegment, ySegment);
2042 }
2043
mapToDevice(const cpp17::any & xValue,const cpp17::any & yValue,int axis,int xSegment,int ySegment)2044 WPointF WCartesianChart::mapToDevice(const cpp17::any &xValue,
2045 const cpp17::any &yValue,
2046 int axis, int xSegment, int ySegment) const
2047 {
2048 return mapToDevice(xValue, yValue, xAxis(0), yAxis(axis), xSegment, ySegment);
2049 }
2050
mapToDevice(const cpp17::any & xValue,const cpp17::any & yValue,const WAxis & xAxis,const WAxis & yAxis,int xSegment,int ySegment)2051 WPointF WCartesianChart::mapToDevice(const cpp17::any &xValue,
2052 const cpp17::any &yValue,
2053 const WAxis &xAxis,
2054 const WAxis &yAxis,
2055 int xSegment, int ySegment) const
2056 {
2057 if (isInteractive()) {
2058 return zoomRangeTransform(
2059 xAxes_[xAxis.xAxis_].transformHandle.value(),
2060 yAxes_[yAxis.yAxis_].transformHandle.value()
2061 ).map(mapToDeviceWithoutTransform(xValue, yValue, xAxis, yAxis, xSegment, ySegment));
2062 } else {
2063 return mapToDeviceWithoutTransform(xValue, yValue, xAxis, yAxis, xSegment, ySegment);
2064 }
2065 }
2066
mapToDeviceWithoutTransform(const cpp17::any & xValue,const cpp17::any & yValue,Axis ordinateAxis,int xSegment,int ySegment)2067 WPointF WCartesianChart::mapToDeviceWithoutTransform(const cpp17::any& xValue,
2068 const cpp17::any& yValue,
2069 Axis ordinateAxis, int xSegment,
2070 int ySegment) const
2071 {
2072 return mapToDeviceWithoutTransform(xValue, yValue, ordinateAxis == Axis::Y1 ? 0 : 1, xSegment, ySegment);
2073 }
2074
mapToDeviceWithoutTransform(const cpp17::any & xValue,const cpp17::any & yValue,int ordinateAxis,int xSegment,int ySegment)2075 WPointF WCartesianChart::mapToDeviceWithoutTransform(const cpp17::any& xValue,
2076 const cpp17::any& yValue,
2077 int ordinateAxis, int xSegment,
2078 int ySegment) const
2079 {
2080 return mapToDeviceWithoutTransform(xValue, yValue, xAxis(0), yAxis(ordinateAxis), xSegment, ySegment);
2081 }
2082
mapToDeviceWithoutTransform(const cpp17::any & xValue,const cpp17::any & yValue,const WAxis & xAxis,const WAxis & yAxis,int xSegment,int ySegment)2083 WPointF WCartesianChart::mapToDeviceWithoutTransform(const cpp17::any &xValue,
2084 const cpp17::any &yValue,
2085 const WAxis &xAxis,
2086 const WAxis &yAxis,
2087 int xSegment,
2088 int ySegment) const
2089 {
2090 double x = chartArea_.left() + xAxis.mapToDevice(xValue, xSegment);
2091 double y = chartArea_.bottom() - yAxis.mapToDevice(yValue, ySegment);
2092
2093 return hv(x, y, width().toPixels());
2094 }
2095
hv(double x,double y,double width)2096 WPointF WCartesianChart::hv(double x, double y,
2097 double width) const
2098 {
2099 if (orientation_ == Orientation::Vertical)
2100 return WPointF(x, y);
2101 else
2102 return WPointF(width - y, x);
2103 }
2104
inverseHv(double x,double y,double width)2105 WPointF WCartesianChart::inverseHv(double x, double y, double width) const
2106 {
2107 if (orientation_ == Orientation::Vertical)
2108 return WPointF(x, y);
2109 else
2110 return WPointF(y, width - x);
2111 }
2112
defineJavaScript()2113 void WCartesianChart::defineJavaScript()
2114 {
2115 WApplication *app = WApplication::instance();
2116
2117 if (app && (isInteractive() || hasDeferredToolTips_)) {
2118 LOAD_JAVASCRIPT(app, "js/ChartCommon.js", "ChartCommon", wtjs2);
2119 app->doJavaScript(std::string("if (!" WT_CLASS ".chartCommon) {"
2120 WT_CLASS ".chartCommon = new ") +
2121 WT_CLASS ".ChartCommon(" + app->javaScriptClass() + "); }", false);
2122
2123 LOAD_JAVASCRIPT(app, "js/WCartesianChart.js", "WCartesianChart", wtjs1);
2124 jsDefined_ = true;
2125 } else {
2126 jsDefined_ = false;
2127 }
2128 }
2129
render(WFlags<RenderFlag> flags)2130 void WCartesianChart::render(WFlags<RenderFlag> flags)
2131 {
2132 WAbstractChart::render(flags);
2133
2134 if (flags.test(RenderFlag::Full) || !jsDefined_) {
2135 defineJavaScript();
2136 }
2137 }
2138
setFormData(const FormData & formData)2139 void WCartesianChart::setFormData(const FormData& formData)
2140 {
2141 WPaintedWidget::setFormData(formData);
2142
2143 std::vector<const WTransform *> xTransforms;
2144 for (std::size_t i = 0; i < xAxes_.size(); ++i) {
2145 xTransforms.push_back(&xAxes_[i].transformHandle.value());
2146 }
2147 std::vector<const WTransform *> yTransforms;
2148 for (std::size_t i = 0; i < yAxes_.size(); ++i) {
2149 yTransforms.push_back(&yAxes_[i].transformHandle.value());
2150 }
2151
2152 for (int i = 0; i < xAxisCount(); ++i) {
2153 if (!xAxis(i).zoomRangeDirty_) {
2154 WPointF devicePan =
2155 WPointF(xTransforms[i]->dx() / xTransforms[i]->m11(), 0.0);
2156 WPointF modelPan = WPointF(xAxis(i).mapFromDevice(-devicePan.x()), 0.0);
2157 if (xTransforms[i]->isIdentity()) {
2158 xAxis(i).setZoomRangeFromClient(WAxis::AUTO_MINIMUM, WAxis::AUTO_MAXIMUM);
2159 } else {
2160 double z = xTransforms[i]->m11();
2161 double x = modelPan.x();
2162 double min = xAxis(i).mapFromDevice(0.0);
2163 double max = xAxis(i).mapFromDevice(xAxis(i).fullRenderLength_);
2164 double x2 = x + ((max - min)/ z);
2165 xAxis(i).setZoomRangeFromClient(x, x2);
2166 }
2167 }
2168 }
2169 for (int i = 0; i < yAxisCount(); ++i) {
2170 if (!yAxis(i).zoomRangeDirty_) {
2171 WPointF devicePan(0.0, yTransforms[i]->dy() / yTransforms[i]->m22());
2172 WPointF modelPan(0.0, yAxis(i).mapFromDevice(-devicePan.y()));
2173 if (yTransforms[i]->isIdentity()) {
2174 yAxis(i).setZoomRangeFromClient(WAxis::AUTO_MINIMUM, WAxis::AUTO_MAXIMUM);
2175 } else {
2176 double z = yTransforms[i]->m22();
2177 double y = modelPan.y();
2178 double min = yAxis(i).mapFromDevice(0.0);
2179 double max = yAxis(i).mapFromDevice(yAxis(i).fullRenderLength_);
2180 double y2 = y + ((max - min) / z);
2181 yAxis(i).setZoomRangeFromClient(y, y2);
2182 }
2183 }
2184 }
2185 if (curveTransforms_.size() != 0) {
2186 for (std::size_t i = 0; i < series_.size(); ++i) {
2187 WDataSeries &s = *series_[i];
2188 int xAxis = s.xAxis();
2189 int yAxis = s.yAxis();
2190 if (xAxis >= 0 && xAxis < xAxisCount() &&
2191 yAxis >= 0 && yAxis < yAxisCount()) {
2192 if ((s.type() == SeriesType::Line || s.type() == SeriesType::Curve) && !s.isHidden()) {
2193 if (!s.scaleDirty_) {
2194 s.scale_ = curveTransforms_[&s].value().m22();
2195 }
2196 if (!s.offsetDirty_) {
2197 double origin;
2198 if (orientation() == Orientation::Horizontal) {
2199 origin = mapToDeviceWithoutTransform(0.0, 0.0, this->xAxis(xAxis), this->yAxis(yAxis)).x();
2200 } else {
2201 origin = mapToDeviceWithoutTransform(0.0, 0.0, this->xAxis(xAxis), this->yAxis(yAxis)).y();
2202 }
2203 double dy = curveTransforms_[&s].value().dy();
2204 double scale = curveTransforms_[&s].value().m22();
2205 double offset = - dy + origin * (1 - scale) + this->yAxis(yAxis).mapToDevice(0.0, 0);
2206 if (orientation() == Orientation::Horizontal) {
2207 s.offset_ = - this->yAxis(yAxis).mapFromDevice(offset);
2208 } else {
2209 s.offset_ = this->yAxis(yAxis).mapFromDevice(offset);
2210 }
2211 }
2212 }
2213 }
2214 }
2215 }
2216 }
2217
modelChanged()2218 void WCartesianChart::modelChanged()
2219 {
2220 XSeriesColumn_ = -1;
2221 while (axisSliderWidgets_.size() > 0) {
2222 axisSliderWidgets_[axisSliderWidgets_.size() - 1]->setSeries(nullptr);
2223 }
2224
2225 freeAllJSPaths();
2226 freeAllJSTransforms();
2227
2228 series_.clear();
2229
2230 update();
2231 }
2232
modelReset()2233 void WCartesianChart::modelReset()
2234 {
2235 update();
2236 }
2237
IconWidget(WCartesianChart * chart,int index)2238 WCartesianChart::IconWidget::IconWidget(WCartesianChart *chart,
2239 int index)
2240 : chart_(chart),
2241 index_(index)
2242 {
2243 setInline(true);
2244 resize(20, 20);
2245 }
2246
paintEvent(Wt::WPaintDevice * paintDevice)2247 void WCartesianChart::IconWidget::paintEvent(Wt::WPaintDevice *paintDevice)
2248 {
2249 Wt::WPainter painter(paintDevice);
2250 chart_->renderLegendIcon(painter,
2251 WPointF(2.5, 10.0),
2252 chart_->series(index_));
2253 }
2254
setAxisPadding(int padding)2255 void WCartesianChart::setAxisPadding(int padding)
2256 {
2257 axisPadding_ = padding;
2258 for (std::size_t i = 0; i < xAxes_.size(); ++i)
2259 xAxes_[i].axis->setPadding(padding);
2260 for (std::size_t i = 0; i < yAxes_.size(); ++i)
2261 yAxes_[i].axis->setPadding(padding);
2262 }
2263
setBorderPen(const WPen & pen)2264 void WCartesianChart::setBorderPen(const WPen& pen)
2265 {
2266 if (borderPen_ != pen) {
2267 borderPen_ = pen;
2268 update();
2269 }
2270 }
2271
setSeriesSelectionEnabled(bool enabled)2272 void WCartesianChart::setSeriesSelectionEnabled(bool enabled)
2273 {
2274 if (seriesSelectionEnabled_ != enabled) {
2275 seriesSelectionEnabled_ = enabled;
2276 updateJSConfig("seriesSelection", seriesSelectionEnabled_);
2277 }
2278 }
2279
setSelectedSeries(const WDataSeries * series)2280 void WCartesianChart::setSelectedSeries(const WDataSeries *series)
2281 {
2282 if (selectedSeries_ != series) {
2283 selectedSeries_ = series;
2284 update();
2285 }
2286 }
2287
setCurveManipulationEnabled(bool enabled)2288 void WCartesianChart::setCurveManipulationEnabled(bool enabled)
2289 {
2290 if (curveManipulationEnabled_ != enabled) {
2291 curveManipulationEnabled_ = enabled;
2292 updateJSConfig("curveManipulation", curveManipulationEnabled_);
2293 }
2294 }
2295
addCurveLabel(const CurveLabel & label)2296 void WCartesianChart::addCurveLabel(const CurveLabel &label)
2297 {
2298 curveLabels_.push_back(label);
2299 update();
2300 }
2301
setCurveLabels(const std::vector<CurveLabel> & labels)2302 void WCartesianChart::setCurveLabels(const std::vector<CurveLabel> &labels)
2303 {
2304 curveLabels_ = labels;
2305 update();
2306 }
2307
clearCurveLabels()2308 void WCartesianChart::clearCurveLabels()
2309 {
2310 curveLabels_.clear();
2311 update();
2312 }
2313
setOnDemandLoadingEnabled(bool enabled)2314 void WCartesianChart::setOnDemandLoadingEnabled(bool enabled)
2315 {
2316 if (onDemandLoadingEnabled_ != enabled) {
2317 onDemandLoadingEnabled_ = enabled;
2318 update();
2319 }
2320 }
2321
setLoadingBackground(const WBrush & brush)2322 void WCartesianChart::setLoadingBackground(const WBrush &brush)
2323 {
2324 if (loadingBackground_ != brush) {
2325 loadingBackground_ = brush;
2326 if (onDemandLoadingEnabled())
2327 update();
2328 }
2329 }
2330
isInteractive()2331 bool WCartesianChart::isInteractive() const
2332 {
2333 return !xAxes_.empty() && !yAxes_.empty() && (zoomEnabled_ || panEnabled_ || crosshairEnabled_ || followCurve_ != 0 ||
2334 axisSliderWidgets_.size() > 0 || seriesSelectionEnabled_ || curveManipulationEnabled_) && getMethod() == RenderMethod::HtmlCanvas;
2335 }
2336
pathForSeries(const WDataSeries & series)2337 WPainterPath WCartesianChart::pathForSeries(const WDataSeries &series) const
2338 {
2339 PainterPathMap::const_iterator it = curvePaths_.find(&series);
2340 if (it == curvePaths_.end()) {
2341 return WPainterPath();
2342 } else {
2343 return it->second.value();
2344 }
2345 }
2346
updateJSPensForAxis(WStringStream & js,Axis axis,int axisId)2347 void WCartesianChart::updateJSPensForAxis(WStringStream& js, Axis axis, int axisId) const
2348 {
2349 const std::vector<PenAssignment> pens =
2350 axis == Axis::X ? xAxes_[axisId].pens : yAxes_[axisId].pens;
2351 js << "[";
2352 for (std::size_t i = 0; i < pens.size(); ++i) {
2353 if (i != 0) {
2354 js << ",";
2355 }
2356 const PenAssignment& assignment = pens[i];
2357 js << "[";
2358 js << assignment.pen.jsRef();
2359 js << ",";
2360 js << assignment.textPen.jsRef();
2361 js << ",";
2362 js << assignment.gridPen.jsRef();
2363 js << "]";
2364 }
2365 js << "]";
2366 }
2367
updateJSPens(WStringStream & js)2368 void WCartesianChart::updateJSPens(WStringStream& js) const
2369 {
2370 // pens[axis][level][]
2371 js << "pens:{x:[";
2372 for (int i = 0; i < xAxisCount(); ++i) {
2373 if (i != 0)
2374 js << ',';
2375 updateJSPensForAxis(js, Axis::X, i);
2376 }
2377 js << "],y:[";
2378 for (int i = 0; i < yAxisCount(); ++i) {
2379 if (i != 0)
2380 js << ',';
2381 updateJSPensForAxis(js, Axis::Y, i);
2382 }
2383 js << "]},";
2384 js << "penAlpha:{x:[";
2385 for (int i = 0; i < xAxisCount(); ++i) {
2386 if (i != 0)
2387 js << ',';
2388 js << '[' << xAxis(i).pen().color().alpha() << ',';
2389 js << xAxis(i).textPen().color().alpha() << ',';
2390 js << xAxis(i).gridLinesPen().color().alpha() << ']';
2391 }
2392 js << "],y:[";
2393 for (int i = 0; i < yAxisCount(); ++i) {
2394 if (i != 0)
2395 js << ',';
2396 js << '[' << yAxis(i).pen().color().alpha() << ',';
2397 js << yAxis(i).textPen().color().alpha() << ',';
2398 js << yAxis(i).gridLinesPen().color().alpha() << ']';
2399 }
2400 js << "]},";
2401 }
2402
calcNumBarGroups()2403 int WCartesianChart::calcNumBarGroups() const
2404 {
2405 int numBarGroups = 0;
2406
2407 bool newGroup = true;
2408 for (unsigned i = 0; i < series_.size(); ++i)
2409 if (series_[i]->type() == SeriesType::Bar) {
2410 if (newGroup || !series_[i]->isStacked())
2411 ++numBarGroups;
2412 newGroup = false;
2413 } else
2414 newGroup = true;
2415
2416 return numBarGroups;
2417 }
2418
setSoftLabelClipping(bool enabled)2419 void WCartesianChart::setSoftLabelClipping(bool enabled)
2420 {
2421 for (std::size_t i = 0; i < xAxes_.size(); ++i)
2422 xAxes_[i].axis->setSoftLabelClipping(enabled);
2423 for (std::size_t i = 0; i < yAxes_.size(); ++i)
2424 yAxes_[i].axis->setSoftLabelClipping(enabled);
2425 }
2426
axisSliderWidgetForSeries(WDataSeries * series)2427 bool WCartesianChart::axisSliderWidgetForSeries(WDataSeries *series) const
2428 {
2429 for (std::size_t i = 0; i < axisSliderWidgets_.size(); ++i) {
2430 if (axisSliderWidgets_[i]->series() == series)
2431 return true;
2432 }
2433 return false;
2434 }
2435
iterateSeries(SeriesIterator * iterator,WPainter * painter,bool reverseStacked,bool extremesOnly)2436 void WCartesianChart::iterateSeries(SeriesIterator *iterator,
2437 WPainter *painter,
2438 bool reverseStacked,
2439 bool extremesOnly) const
2440 {
2441 double groupWidth = 0.0;
2442 int numBarGroups;
2443 int currentBarGroup;
2444
2445 int rowCount = model() ? model()->rowCount() : 0;
2446 std::vector<double> posStackedValuesInit, minStackedValuesInit;
2447
2448 const bool scatterPlot = type_ == ChartType::Scatter;
2449
2450 if (scatterPlot) {
2451 numBarGroups = 1;
2452 currentBarGroup = 0;
2453 } else {
2454 numBarGroups = calcNumBarGroups();
2455 currentBarGroup = 0;
2456 posStackedValuesInit.insert(posStackedValuesInit.begin(), rowCount, 0.0);
2457 minStackedValuesInit.insert(minStackedValuesInit.begin(), rowCount, 0.0);
2458 }
2459
2460 bool containsBars = false;
2461
2462 for (unsigned g = 0; g < series_.size(); ++g) {
2463 if (series_[g]->isHidden() &&
2464 !(axisSliderWidgetForSeries(series_[g].get()) &&
2465 (dynamic_cast<SeriesRenderIterator *>(iterator) ||
2466 dynamic_cast<ExtremesIterator *>(iterator))))
2467 continue;
2468
2469 groupWidth = series_[g]->barWidth() * (xAxis(series_[g]->xAxis()).mapToDevice(2) - xAxis(series_[g]->xAxis()).mapToDevice(1));
2470
2471 if (containsBars)
2472 ++currentBarGroup;
2473 containsBars = false;
2474
2475 int startSeries, endSeries;
2476
2477 if (scatterPlot) {
2478 startSeries = endSeries = g;
2479 } else if (series_[g]->model() == model()) {
2480 for (int i = 0; i < rowCount; ++i)
2481 posStackedValuesInit[i] = minStackedValuesInit[i] = 0.0;
2482
2483 if (reverseStacked) {
2484 endSeries = g;
2485
2486 int xAxis = series_[g]->xAxis();
2487 int yAxis = series_[g]->yAxis();
2488
2489 for (;;) {
2490 if (g < series_.size()
2491 && (((int)g == endSeries) || series_[g]->isStacked())
2492 && (series_[g]->xAxis() == xAxis)
2493 && (series_[g]->yAxis() == yAxis)) {
2494 if (series_[g]->type() == SeriesType::Bar)
2495 containsBars = true;
2496
2497 for (int row = 0; row < rowCount; ++row) {
2498 double y = asNumber(model()->data(row, series_[g]->modelColumn()));
2499
2500 if (!Utils::isNaN(y)) {
2501 if (y > 0)
2502 posStackedValuesInit[row] += y;
2503 else
2504 minStackedValuesInit[row] += y;
2505 }
2506 }
2507
2508 ++g;
2509 } else
2510 break;
2511 }
2512
2513 --g;
2514 startSeries = g;
2515 } else {
2516 startSeries = g;
2517
2518 int xAxis = series_[g]->xAxis();
2519 int yAxis = series_[g]->yAxis();
2520
2521 if (series_[g]->type() == SeriesType::Bar)
2522 containsBars = true;
2523 ++g;
2524
2525 for (;;) {
2526 if (g < series_.size() && series_[g]->isStacked()
2527 && series_[g]->xAxis() == xAxis
2528 && series_[g]->yAxis() == yAxis) {
2529 if (series_[g]->type() == SeriesType::Bar)
2530 containsBars = true;
2531 ++g;
2532 } else
2533 break;
2534 }
2535
2536 --g;
2537
2538 endSeries = g;
2539 }
2540 } else {
2541 throw WException("Configuring different models for WDataSeries are unsupported "
2542 "for category charts!");
2543 }
2544
2545 int i = startSeries;
2546 for (;;) {
2547 bool doSeries = series_[i]->xAxis() >= 0 && series_[i]->xAxis() < xAxisCount() &&
2548 series_[i]->yAxis() >= 0 && series_[i]->yAxis() < yAxisCount() &&
2549 iterator->startSeries(*series_[i], groupWidth, numBarGroups,
2550 currentBarGroup);
2551
2552 std::vector<double> posStackedValues, minStackedValues;
2553
2554 if (doSeries ||
2555 (!scatterPlot && i != endSeries)) {
2556
2557 for (int currentXSegment = 0;
2558 currentXSegment < xAxis(series_[i]->xAxis()).segmentCount();
2559 ++currentXSegment) {
2560
2561 for (int currentYSegment = 0;
2562 currentYSegment < yAxis(series_[i]->yAxis()).segmentCount();
2563 ++currentYSegment) {
2564
2565 posStackedValues.clear();
2566 Utils::insert(posStackedValues, posStackedValuesInit);
2567 minStackedValues.clear();
2568 Utils::insert(minStackedValues, minStackedValuesInit);
2569
2570 if (painter) {
2571 WRectF csa = chartSegmentArea(xAxis(series_[i]->xAxis()),
2572 yAxis(series_[i]->yAxis()),
2573 currentXSegment,
2574 currentYSegment);
2575 iterator->startSegment(currentXSegment, currentYSegment, csa);
2576
2577 painter->save();
2578
2579 if (!isInteractive()) {
2580 WPainterPath clipPath;
2581
2582 clipPath.addRect(hv(csa));
2583 painter->setClipPath(clipPath);
2584 painter->setClipping(true);
2585 }
2586 } else {
2587 iterator->startSegment(currentXSegment, currentYSegment,
2588 WRectF());
2589 }
2590
2591 int startRow = 0;
2592 int endRow = series_[i]->model() ? series_[i]->model()->rowCount() : 0;
2593
2594 if (isInteractive() &&
2595 !extremesOnly &&
2596 onDemandLoadingEnabled() &&
2597 series_[i]->model() &&
2598 !axisSliderWidgetForSeries(series_[i].get())) {
2599 int xColumn = series_[i]->XSeriesColumn() == -1 ? XSeriesColumn() : series_[i]->XSeriesColumn();
2600 double zoomMin = xAxis(series_[i]->xAxis()).zoomMinimum();
2601 double zoomMax = xAxis(series_[i]->xAxis()).zoomMaximum();
2602 double zoomRange = zoomMax - zoomMin;
2603 if (xColumn == -1) {
2604 startRow = std::max(0, static_cast<int>(zoomMin - zoomRange));
2605 endRow = std::min(endRow, static_cast<int>(std::ceil(zoomMax + zoomRange)) + 1);
2606 } else {
2607 startRow = std::max(binarySearchRow(*series_[i]->model(),
2608 xColumn,
2609 zoomMin - zoomRange,
2610 0, series_[i]->model()->rowCount() - 1) - 1, startRow);
2611 endRow = std::min(binarySearchRow(*series_[i]->model(),
2612 xColumn,
2613 zoomMax + zoomRange,
2614 0, series_[i]->model()->rowCount() - 1) + 1, endRow);
2615 }
2616 }
2617
2618 for (int row = startRow; row < endRow; ++row) {
2619 int xIndex[] = {-1, -1};
2620 int yIndex[] = {-1, -1};
2621
2622 double x;
2623 if (scatterPlot) {
2624 int c = series_[i]->XSeriesColumn();
2625 if (c == -1)
2626 c = XSeriesColumn();
2627 if (c != -1) {
2628 xIndex[0] = row;
2629 xIndex[1] = c;
2630 x = series_[i]->model()->data(xIndex[0], xIndex[1]);
2631 } else
2632 x = row;
2633 } else
2634 x = row;
2635
2636 yIndex[0] = row;
2637 yIndex[1] = series_[i]->modelColumn();
2638 double y = series_[i]->model()->data(yIndex[0], yIndex[1]);
2639
2640 if (scatterPlot)
2641 iterator->newValue(*series_[i], x, y, 0,
2642 xIndex[0], xIndex[1], yIndex[0], yIndex[1]);
2643 else {
2644 double prevStack = 0, nextStack = 0;
2645
2646 bool hasValue = !Utils::isNaN(y);
2647
2648 if (hasValue) {
2649 if (y > 0)
2650 prevStack = nextStack = posStackedValues[row];
2651 else
2652 prevStack = nextStack = minStackedValues[row];
2653
2654 if (reverseStacked)
2655 nextStack -= y;
2656 else
2657 nextStack += y;
2658
2659 if (y > 0)
2660 posStackedValues[row] = nextStack;
2661 else
2662 minStackedValues[row] = nextStack;
2663 }
2664
2665 if (doSeries) {
2666 if (reverseStacked)
2667 iterator->newValue(*series_[i], x, hasValue ? prevStack : y,
2668 nextStack,
2669 xIndex[0], xIndex[1], yIndex[0], yIndex[1]);
2670 else
2671 iterator->newValue(*series_[i], x, hasValue ? nextStack : y,
2672 prevStack,
2673 xIndex[0], xIndex[1], yIndex[0], yIndex[1]);
2674 }
2675 }
2676
2677 if (extremesOnly && onDemandLoadingEnabled())
2678 row = std::max(endRow - 2, row);
2679 }
2680
2681 iterator->endSegment();
2682
2683 if (painter)
2684 painter->restore();
2685 }
2686 }
2687
2688 posStackedValuesInit.clear();
2689 Utils::insert(posStackedValuesInit, posStackedValues);
2690 minStackedValuesInit.clear();
2691 Utils::insert(minStackedValuesInit, minStackedValues);
2692 }
2693
2694 if (doSeries)
2695 iterator->endSeries();
2696
2697 if (i == endSeries)
2698 break;
2699 else {
2700 if (endSeries < startSeries)
2701 --i;
2702 else
2703 ++i;
2704 }
2705 }
2706 }
2707 }
2708
2709 /*
2710 * ------ Rendering logic.
2711 */
2712
paint(WPainter & painter,const WRectF & rectangle)2713 void WCartesianChart::paint(WPainter& painter, const WRectF& rectangle) const
2714 {
2715
2716 for (WAbstractArea *area: areas())
2717 const_cast<WCartesianChart *>(this)->removeArea(area);
2718
2719 if (!painter.isActive())
2720 throw WException("WCartesianChart::paint(): painter is not active.");
2721
2722 WRectF rect = rectangle;
2723
2724 if (rect.isNull() || rect.isEmpty())
2725 rect = painter.window();
2726
2727 render(painter, rect);
2728 }
2729
insideChartArea()2730 WRectF WCartesianChart::insideChartArea() const
2731 {
2732 // margin used when clipping, see also WAxis::prepareRender(),
2733 // when the renderMinimum/maximum is 0, clipping is done exact
2734 double xRenderStart = 0;
2735 double xRenderEnd = 0;
2736 if (xAxisCount() >= 1) {
2737 const WAxis &xAxis = this->xAxis(0);
2738 const WAxis::Segment &xs = xAxis.segments_[0];
2739 xRenderStart = xAxis.inverted() ? xAxis.mapToDevice(xs.renderMaximum, 0) : xs.renderStart;
2740 xRenderEnd = xAxis.inverted() ? xAxis.mapToDevice(xs.renderMinimum, 0) : xs.renderStart + xs.renderLength;
2741 }
2742
2743 double yRenderStart = 0;
2744 double yRenderEnd = 0;
2745 if (yAxisCount() >= 1) {
2746 const WAxis& yAxis = this->yAxis(0);
2747 const WAxis::Segment& ys = yAxis.segments_[0];
2748 yRenderStart = yAxis.inverted() ? yAxis.mapToDevice(ys.renderMaximum, 0) : ys.renderStart;
2749 yRenderEnd = yAxis.inverted() ? yAxis.mapToDevice(ys.renderMinimum, 0) : ys.renderStart + ys.renderLength;
2750 }
2751
2752 double x1 = chartArea_.left() + xRenderStart;
2753 double x2 = chartArea_.left() + xRenderEnd;
2754 double y1 = chartArea_.bottom() - yRenderEnd;
2755 double y2 = chartArea_.bottom() - yRenderStart;
2756
2757 return WRectF(x1, y1, x2 - x1, y2 - y1);
2758 }
2759
setZoomAndPan()2760 void WCartesianChart::setZoomAndPan()
2761 {
2762 std::vector<WTransform> xTransforms;
2763 for (int i = 0; i < xAxisCount(); ++i) {
2764 if (xAxis(i).zoomMin_ != WAxis::AUTO_MINIMUM ||
2765 xAxis(i).zoomMax_ != WAxis::AUTO_MAXIMUM) {
2766 double xPan = - xAxis(i).mapToDevice(xAxis(i).pan(), 0);
2767 double xZoom = xAxis(i).zoom();
2768 if (xZoom > xAxis(i).maxZoom())
2769 xZoom = xAxis(i).maxZoom();
2770 if (xZoom < xAxis(i).minZoom())
2771 xZoom = xAxis(i).minZoom();
2772 xTransforms.push_back(WTransform(xZoom, 0, 0, 1, xZoom * xPan, 0));
2773 } else {
2774 double xZoom = xAxis(i).minZoom();
2775 xTransforms.push_back(WTransform(xZoom, 0, 0, 1, 0, 0));
2776 }
2777 }
2778
2779 std::vector<WTransform> yTransforms;
2780 for (int i = 0; i < yAxisCount(); ++i) {
2781 if (yAxis(i).zoomMin_ != WAxis::AUTO_MINIMUM ||
2782 yAxis(i).zoomMax_ != WAxis::AUTO_MAXIMUM) {
2783 double yPan = -yAxis(i).mapToDevice(yAxis(i).pan(), 0);
2784 double yZoom = yAxis(i).zoom();
2785 if (yZoom > yAxis(i).maxZoom())
2786 yZoom = yAxis(i).maxZoom();
2787 if (yZoom < yAxis(i).minZoom())
2788 yZoom = yAxis(i).minZoom();
2789 yTransforms.push_back(WTransform(1, 0, 0, yZoom, 0, yZoom * yPan));
2790 } else {
2791 double yZoom = yAxis(i).minZoom();
2792 yTransforms.push_back(WTransform(1, 0, 0, yZoom, 0, 0));
2793 }
2794 }
2795
2796 // Enforce limits
2797 WRectF chartArea = hv(insideChartArea());
2798 for (int i = 0; i < xAxisCount(); ++i) {
2799 WRectF transformedArea = zoomRangeTransform(xTransforms[i], WTransform()).map(chartArea);
2800 if (orientation() == Orientation::Vertical) {
2801 if (transformedArea.left() > chartArea.left()) {
2802 double diff = chartArea.left() - transformedArea.left();
2803 xTransforms[i] = WTransform(1, 0, 0, 1, diff, 0) * xTransforms[i];
2804 } else if (transformedArea.right() < chartArea.right()) {
2805 double diff = chartArea.right() - transformedArea.right();
2806 xTransforms[i] = WTransform(1, 0, 0, 1, diff, 0) * xTransforms[i];
2807 }
2808 } else {
2809 if (transformedArea.top() > chartArea.top()) {
2810 double diff = chartArea.top() - transformedArea.top();
2811 xTransforms[i] = WTransform(1, 0, 0, 1, diff, 0) * xTransforms[i];
2812 } else if (transformedArea.bottom() < chartArea.bottom()) {
2813 double diff = chartArea.bottom() - transformedArea.bottom();
2814 xTransforms[i] = WTransform(1, 0, 0, 1, diff, 0) * xTransforms[i];
2815 }
2816 }
2817 }
2818 for (int i = 0; i < yAxisCount(); ++i) {
2819 WRectF transformedArea = zoomRangeTransform(WTransform(), yTransforms[i]).map(chartArea);
2820 if (orientation() == Orientation::Vertical) {
2821 if (transformedArea.top() > chartArea.top()) {
2822 double diff = chartArea.top() - transformedArea.top();
2823 yTransforms[i] = WTransform(1, 0, 0, 1, 0, -diff) * yTransforms[i];
2824 } else if (transformedArea.bottom() < chartArea.bottom()) {
2825 double diff = chartArea.bottom() - transformedArea.bottom();
2826 yTransforms[i] = WTransform(1, 0, 0, 1, 0, -diff) * yTransforms[i];
2827 }
2828 } else {
2829 if (transformedArea.left() > chartArea.left()) {
2830 double diff = chartArea.left() - transformedArea.left();
2831 yTransforms[i] = WTransform(1, 0, 0, 1, 0, diff) * yTransforms[i];
2832 } else if (transformedArea.right() < chartArea.right()) {
2833 double diff = chartArea.right() - transformedArea.right();
2834 yTransforms[i] = WTransform(1, 0, 0, 1, 0, diff) * yTransforms[i];
2835 }
2836 }
2837 }
2838
2839 for (int i = 0; i < xAxisCount(); ++i) {
2840 xAxes_[i].transformHandle.setValue(xTransforms[i]);
2841 }
2842 for (int i = 0; i < yAxisCount(); ++i) {
2843 yAxes_[i].transformHandle.setValue(yTransforms[i]);
2844 }
2845
2846 for (int i = 0; i < xAxisCount(); ++i) {
2847 xAxis(i).zoomRangeDirty_ = false;
2848 }
2849 for (int i = 0; i < yAxisCount(); ++i) {
2850 yAxis(i).zoomRangeDirty_ = false;
2851 }
2852 }
2853
paintEvent(WPaintDevice * paintDevice)2854 void WCartesianChart::paintEvent(WPaintDevice *paintDevice)
2855 {
2856 hasDeferredToolTips_ = false;
2857
2858 WPainter painter(paintDevice);
2859 painter.setRenderHint(RenderHint::Antialiasing);
2860 paint(painter);
2861
2862 if (hasDeferredToolTips_ && !jsDefined_) {
2863 // We need to define the JavaScript after all, because we need to be able
2864 // to load deferred tooltips.
2865 defineJavaScript();
2866 }
2867
2868 if (isInteractive() || hasDeferredToolTips_) {
2869 setZoomAndPan();
2870
2871 std::vector<WRectF> xModelAreas;
2872 std::vector<WRectF> yModelAreas;
2873
2874 for (int i = 0; i < xAxisCount(); ++i) {
2875 double modelBottom = yAxis(0).mapFromDevice(0);
2876 double modelTop = yAxis(0).mapFromDevice(chartArea_.height());
2877 double modelLeft = xAxis(i).mapFromDevice(0);
2878 double modelRight = xAxis(i).mapFromDevice(chartArea_.width());
2879 WRectF modelArea(modelLeft, modelBottom, modelRight - modelLeft, modelTop - modelBottom);
2880 xModelAreas.push_back(modelArea);
2881 }
2882 for (int i = 0; i < yAxisCount(); ++i) {
2883 double modelBottom = yAxis(i).mapFromDevice(0);
2884 double modelTop = yAxis(i).mapFromDevice(chartArea_.height());
2885 double modelLeft = axis(Axis::X).mapFromDevice(0);
2886 double modelRight = axis(Axis::X).mapFromDevice(chartArea_.width());
2887 WRectF modelArea(modelLeft, modelBottom, modelRight - modelLeft, modelTop - modelBottom);
2888 yModelAreas.push_back(modelArea);
2889 }
2890
2891 WRectF insideArea = insideChartArea();
2892
2893 int coordPaddingX = 5,
2894 coordPaddingY = 5;
2895
2896 if (orientation() == Orientation::Vertical) {
2897 for (int i = 0; i < xAxisCount(); ++i) {
2898 if (xAxis(i).isVisible() &&
2899 (xAxes_[i].location.initLoc == AxisValue::Maximum ||
2900 xAxes_[i].location.initLoc == AxisValue::Both)) {
2901 if (xAxis(i).tickDirection() == TickDirection::Inwards)
2902 coordPaddingY = 25;
2903 break;
2904 }
2905 }
2906 for (int i = 0; i < yAxisCount(); ++i) {
2907 if (yAxis(i).isVisible() &&
2908 (yAxes_[i].location.initLoc == AxisValue::Maximum ||
2909 yAxes_[i].location.initLoc == AxisValue::Both)) {
2910 if (yAxis(i).tickDirection() == TickDirection::Inwards)
2911 coordPaddingX = 40;
2912 break;
2913 }
2914 }
2915 } else {
2916 for (int i = 0; i < xAxisCount(); ++i) {
2917 if (xAxis(i).isVisible() &&
2918 (xAxes_[i].location.initLoc == AxisValue::Maximum ||
2919 xAxes_[i].location.initLoc == AxisValue::Both)) {
2920 if (xAxis(i).tickDirection() == TickDirection::Inwards)
2921 coordPaddingX = 40;
2922 break;
2923 }
2924 }
2925 for (int i = 0; i < yAxisCount(); ++i) {
2926 if (yAxis(i).isVisible() &&
2927 (yAxes_[i].location.initLoc == AxisValue::Minimum ||
2928 yAxes_[i].location.initLoc == AxisValue::Both)) {
2929 if (yAxis(i).tickDirection() == TickDirection::Inwards)
2930 coordPaddingY = 25;
2931 break;
2932 }
2933 }
2934 }
2935
2936 for (int i = 0; i < xAxisCount(); ++i) {
2937 if ((xAxis(i).zoomRangeChanged().isConnected() ||
2938 onDemandLoadingEnabled()) &&
2939 !xAxes_[i].transformChanged->isConnected()) {
2940 const int axis = i; // Fix for JWt
2941 xAxes_[i].transformChanged->connect(this, std::bind(&WCartesianChart::xTransformChanged, this, axis));
2942 }
2943 }
2944 for (int i = 0; i < yAxisCount(); ++i) {
2945 if ((yAxis(i).zoomRangeChanged().isConnected() ||
2946 onDemandLoadingEnabled()) &&
2947 !yAxes_[i].transformChanged->isConnected()) {
2948 const int axis = i; // Fix for JWt
2949 yAxes_[i].transformChanged->connect(this, std::bind(&WCartesianChart::yTransformChanged, this, axis));
2950 }
2951 }
2952
2953 char buf[30];
2954 WApplication *app = WApplication::instance();
2955 WStringStream ss;
2956 int selectedCurve = selectedSeries_ ? seriesIndexOf(*selectedSeries_) : -1;
2957 int followCurve = followCurve_ ? seriesIndexOf(*followCurve_) : -1;
2958 ss << "new " WT_CLASS ".WCartesianChart("
2959 << app->javaScriptClass() << ","
2960 << jsRef() << ","
2961 << objJsRef() << ","
2962 "{"
2963 "curveManipulation:" << asString(curveManipulationEnabled_).toUTF8() << ","
2964 "seriesSelection:" << asString(seriesSelectionEnabled_).toUTF8() << ","
2965 "selectedCurve:" << selectedCurve << ","
2966 "isHorizontal:" << asString(orientation() == Orientation::Horizontal)
2967 .toUTF8() << ","
2968 "zoom:" << asString(zoomEnabled_).toUTF8() << ","
2969 "pan:" << asString(panEnabled_).toUTF8() << ","
2970 "crosshair:" << asString(crosshairEnabled_).toUTF8() << ","
2971 "crosshairXAxis:" << crosshairXAxis_ << ","
2972 "crosshairYAxis:" << crosshairYAxis_ << ","
2973 "crosshairColor:" << jsStringLiteral(crosshairColor_.cssText(true)) << ","
2974 "followCurve:" << followCurve << ","
2975 "xTransforms:[";
2976 for (int i = 0; i < xAxisCount(); ++i) {
2977 if (i != 0)
2978 ss << ',';
2979 ss << xAxes_[i].transformHandle.jsRef();
2980 }
2981 ss << "],"
2982 "yTransforms:[";
2983 for (int i = 0; i < yAxisCount(); ++i) {
2984 if (i != 0)
2985 ss << ',';
2986 ss << yAxes_[i].transformHandle.jsRef();
2987 }
2988 ss << "],"
2989 "area:" << hv(chartArea_).jsRef() << ","
2990 "insideArea:" << hv(insideArea).jsRef() << ","
2991 "xModelAreas:[";
2992 for (int i = 0; i < xAxisCount(); ++i) {
2993 if (i != 0)
2994 ss << ',';
2995 ss << xModelAreas[i].jsRef();
2996 }
2997 ss << "],"
2998 "yModelAreas:[";
2999 for (int i = 0; i < yAxisCount(); ++i) {
3000 if (i != 0)
3001 ss << ',';
3002 ss << yModelAreas[i].jsRef();
3003 }
3004 ss << "],";
3005 ss << "hasToolTips:" << asString(hasDeferredToolTips_).toUTF8() << ","
3006 "notifyTransform:{x:[";
3007 for (int i = 0; i < xAxisCount(); ++i) {
3008 if (i != 0)
3009 ss << ',';
3010 ss << asString(xAxis(i).zoomRangeChanged().isConnected() ||
3011 onDemandLoadingEnabled()).toUTF8();
3012 }
3013 ss << "], y:[";
3014 for (int i = 0; i < yAxisCount(); ++i) {
3015 if (i != 0)
3016 ss << ',';
3017 ss << asString(yAxis(i).zoomRangeChanged().isConnected() ||
3018 onDemandLoadingEnabled()).toUTF8();
3019 }
3020 ss << "]},"
3021 "ToolTipInnerStyle:" << jsStringLiteral(app->theme()->utilityCssClass(ToolTipInner)) << ","
3022 "ToolTipOuterStyle:" << jsStringLiteral(app->theme()->utilityCssClass(ToolTipOuter)) << ",";
3023 updateJSPens(ss);
3024 ss << "series:{";
3025 {
3026 bool firstCurvePath = true;
3027 for (std::size_t i = 0; i < series_.size(); ++i) {
3028 if (curvePaths_.find(series_[i].get()) != curvePaths_.end()) {
3029 if (firstCurvePath) {
3030 firstCurvePath = false;
3031 } else {
3032 ss << ",";
3033 }
3034 ss << (int)i << ":{";
3035 ss << "curve:" << curvePaths_[series_[i].get()].jsRef() << ",";
3036 ss << "transform:" << curveTransforms_[series_[i].get()].jsRef() << ",";
3037 ss << "xAxis:" << series_[i]->xAxis() << ',';
3038 ss << "yAxis:" << series_[i]->yAxis();
3039 ss << "}";
3040 }
3041 }
3042 }
3043 ss << "},";
3044 ss << "minZoom:{x:[";
3045 for (int i = 0; i < xAxisCount(); ++i) {
3046 if (i != 0)
3047 ss << ',';
3048 ss << Utils::round_js_str(xAxis(i).minZoom(), 16, buf);
3049 }
3050 ss << "],y:[";
3051 for (int i = 0; i < yAxisCount(); ++i) {
3052 if (i != 0)
3053 ss << ',';
3054 ss << Utils::round_js_str(yAxis(i).minZoom(), 16, buf);
3055 }
3056 ss << "]},";
3057 ss << "maxZoom:{x:[";
3058 for (int i = 0; i < xAxisCount(); ++i) {
3059 if (i != 0)
3060 ss << ',';
3061 ss << Utils::round_js_str(xAxis(i).maxZoom(), 16, buf);
3062 }
3063 ss << "],y:[";
3064 for (int i = 0; i < yAxisCount(); ++i) {
3065 if (i != 0)
3066 ss << ',';
3067 ss << Utils::round_js_str(yAxis(i).maxZoom(), 16, buf);
3068 }
3069 ss << "]},";
3070 ss << "rubberBand:" << rubberBandEnabled_ << ',';
3071 ss << "sliders:[";
3072 for (std::size_t i = 0; i < axisSliderWidgets_.size(); ++i) {
3073 if (i != 0) ss << ',';
3074 ss << '"' << axisSliderWidgets_[i]->id() << '"';
3075 }
3076 ss << "],";
3077 ss << "wheelActions:" << wheelActionsToJson(wheelActions_) << ",";
3078 ss << "coordinateOverlayPadding:[" << coordPaddingX << ",";
3079 ss << coordPaddingY << "],";
3080 ss << "xAxes:[";
3081 for (int i = 0; i < xAxisCount(); ++i) {
3082 if (i != 0)
3083 ss << ',';
3084 ss << '{';
3085 ss << "width:" << Utils::round_js_str(xAxes_[i].calculatedWidth, 16, buf) << ',';
3086 ss << "side:'" << locToJsString(xAxes_[i].location.initLoc) << "',";
3087 ss << "minOffset:" << xAxes_[i].location.minOffset << ',';
3088 ss << "maxOffset:" << xAxes_[i].location.maxOffset;
3089 ss << '}';
3090 }
3091 ss << "],";
3092 ss << "yAxes:[";
3093 for (int i = 0; i < yAxisCount(); ++i) {
3094 if (i != 0)
3095 ss << ',';
3096 ss << '{';
3097 ss << "width:" << Utils::round_js_str(yAxes_[i].calculatedWidth, 16, buf) << ',';
3098 ss << "side:'" << locToJsString(yAxes_[i].location.initLoc) << "',";
3099 ss << "minOffset:" << yAxes_[i].location.minOffset << ',';
3100 ss << "maxOffset:" << yAxes_[i].location.maxOffset;
3101 ss << '}';
3102 }
3103 ss << "]});";
3104
3105 doJavaScript(ss.str());
3106
3107 cObjCreated_ = true;
3108 }
3109 }
3110
getDomChanges(std::vector<DomElement * > & result,WApplication * app)3111 void WCartesianChart::getDomChanges(std::vector<DomElement *>& result, WApplication *app)
3112 {
3113 WAbstractChart::getDomChanges(result, app);
3114 for (std::size_t i = 0; i < axisSliderWidgets_.size(); ++i) {
3115 axisSliderWidgets_[i]->update();
3116 }
3117 }
3118
render(WPainter & painter,const WRectF & rectangle)3119 void WCartesianChart::render(WPainter& painter, const WRectF& rectangle) const
3120 {
3121 painter.save();
3122 painter.translate(rectangle.topLeft());
3123
3124 if (initLayout(rectangle, painter.device())) {
3125 renderBackground(painter);
3126 for (int i = 0; i < xAxisCount(); ++i)
3127 renderGrid(painter, xAxis(i));
3128 for (int i = 0; i < yAxisCount(); ++i)
3129 renderGrid(painter, yAxis(i));
3130 renderAxes(painter, AxisProperty::Line); // render the axes (lines)
3131 renderSeries(painter); // render the data series
3132 renderAxes(painter, AxisProperty::Labels); // render the axes (labels)
3133 renderBorder(painter);
3134 renderCurveLabels(painter);
3135 renderLegend(painter);
3136 renderOther(painter);
3137 }
3138
3139 painter.restore();
3140 }
3141
initLayout(const WRectF & rectangle,WPaintDevice * device)3142 bool WCartesianChart::initLayout(const WRectF& rectangle, WPaintDevice *device)
3143 const
3144 {
3145 if (xAxisCount() == 0 || yAxisCount() == 0)
3146 return false;
3147
3148 WRectF rect = rectangle;
3149 if (rect.isNull() || rect.isEmpty())
3150 rect = WRectF(0.0, 0.0, width().toPixels(), height().toPixels());
3151
3152 if (orientation() == Orientation::Vertical) {
3153 width_ = (int)rect.width();
3154 height_ = (int)rect.height();
3155 } else {
3156 width_ = (int)rect.height();
3157 height_ = (int)rect.width();
3158 }
3159
3160 for (std::size_t i = 0; i < xAxes_.size(); ++i) {
3161 xAxes_[i].location.initLoc = AxisValue::Minimum;
3162 xAxes_[i].location.finLoc = AxisValue::Minimum;
3163 }
3164 for (std::size_t i = 0; i < yAxes_.size(); ++i) {
3165 yAxes_[i].location.initLoc = AxisValue::Minimum;
3166 yAxes_[i].location.finLoc = AxisValue::Minimum;
3167 }
3168
3169 std::unique_ptr<WPaintDevice> created;
3170 WPaintDevice *d = device;
3171 if (!d) {
3172 created = createPaintDevice();
3173 d = created.get();
3174 }
3175
3176 bool autoLayout = isAutoLayoutEnabled();
3177 if (autoLayout &&
3178 ((d->features() & PaintDeviceFeatureFlag::FontMetrics).empty())) {
3179 LOG_ERROR("setAutoLayout(): device does not have font metrics "
3180 "(not even server-side font metrics).");
3181 autoLayout = false;
3182 }
3183
3184 // FIXME: eliminate this const_cast!
3185 WCartesianChart *self = const_cast<WCartesianChart *>(this);
3186 self->clearPens();
3187 if (isInteractive()) {
3188 for (int i = 0; i < xAxisCount(); ++i) {
3189 self->createPensForAxis(Axis::X, i);
3190 }
3191 for (int i = 0; i < yAxisCount(); ++i) {
3192 self->createPensForAxis(Axis::Y, i);
3193 }
3194 }
3195
3196 if (autoLayout) {
3197 self->setPlotAreaPadding(40, WFlags<Side>(Side::Left) | Side::Right);
3198 self->setPlotAreaPadding(30, WFlags<Side>(Side::Top) | Side::Bottom);
3199
3200 calcChartArea();
3201
3202 for (int i = 0; i < xAxisCount(); ++i)
3203 xAxes_[i].transform = WTransform();
3204 for (int i = 0; i < yAxisCount(); ++i)
3205 yAxes_[i].transform = WTransform();
3206
3207 if (chartArea_.width() <= 5 || chartArea_.height() <= 5 || !prepareAxes(device)) {
3208 if (isInteractive()) {
3209 for (int i = 0; i < xAxisCount(); ++i) {
3210 xAxes_[i].transform = xAxes_[i].transformHandle.value();
3211 }
3212 for (int i = 0; i < yAxisCount(); ++i) {
3213 yAxes_[i].transform = yAxes_[i].transformHandle.value();
3214 }
3215 }
3216 return false;
3217 }
3218
3219 {
3220 WMeasurePaintDevice md(d);
3221 WPainter painter(&md);
3222
3223 renderAxes(painter, AxisProperty::Line | AxisProperty::Labels);
3224 renderLegend(painter);
3225
3226 WRectF bounds = md.boundingRect();
3227
3228 /* bounds should be within rect with a 5 pixel margin */
3229 const int MARGIN = 5;
3230 int corrLeft = (int)std::max(0.0,
3231 rect.left() - bounds.left() + MARGIN);
3232 int corrRight = (int)std::max(0.0,
3233 bounds.right() - rect.right() + MARGIN);
3234 int corrTop = (int)std::max(0.0, rect.top() - bounds.top() + MARGIN);
3235 int corrBottom = (int)std::max(0.0, bounds.bottom() - rect.bottom()
3236 + MARGIN);
3237
3238 self->setPlotAreaPadding(plotAreaPadding(Side::Left) + corrLeft,
3239 Side::Left);
3240 self->setPlotAreaPadding(plotAreaPadding(Side::Right) + corrRight,
3241 Side::Right);
3242 self->setPlotAreaPadding(plotAreaPadding(Side::Top) + corrTop,
3243 Side::Top);
3244 self->setPlotAreaPadding(plotAreaPadding(Side::Bottom) + corrBottom,
3245 Side::Bottom);
3246 }
3247 }
3248
3249 created.reset();
3250
3251 calcChartArea();
3252
3253 bool result = chartArea_.width() > 5 && chartArea_.height() > 5 && prepareAxes(device);
3254
3255 if (isInteractive()) {
3256 for (int i = 0; i < xAxisCount(); ++i)
3257 xAxes_[i].transform = xAxes_[i].transformHandle.value();
3258 for (int i = 0; i < yAxisCount(); ++i)
3259 yAxes_[i].transform = yAxes_[i].transformHandle.value();
3260 } else {
3261 for (int i = 0; i < xAxisCount(); ++i)
3262 xAxes_[i].transform = WTransform();
3263 for (int i = 0; i < yAxisCount(); ++i)
3264 yAxes_[i].transform = WTransform();
3265 }
3266
3267 if (isInteractive()) {
3268 if (curvePaths_.empty()) {
3269 self->assignJSHandlesForAllSeries();
3270 }
3271 }
3272
3273 return result;
3274 }
3275
drawMarker(const WDataSeries & series,WPainterPath & result)3276 void WCartesianChart::drawMarker(const WDataSeries& series,
3277 WPainterPath& result) const
3278 {
3279 drawMarker(series, series.marker(), result);
3280 }
3281
drawMarker(const WDataSeries & series,MarkerType marker,WPainterPath & result)3282 void WCartesianChart::drawMarker(const WDataSeries &series,
3283 MarkerType marker,
3284 WPainterPath &result) const
3285 {
3286 const double size = 6.0;
3287 const double hsize = size/2;
3288
3289 switch (marker) {
3290 case MarkerType::Circle:
3291 result.addEllipse(-hsize, -hsize, size, size);
3292 break;
3293 case MarkerType::Square:
3294 result.addRect(WRectF(-hsize, -hsize, size, size));
3295 break;
3296 case MarkerType::Cross:
3297 result.moveTo(-1.3 * hsize, 0);
3298 result.lineTo(1.3 * hsize, 0);
3299 result.moveTo(0, -1.3 * hsize);
3300 result.lineTo(0, 1.3 * hsize);
3301 break;
3302 case MarkerType::XCross:
3303 result.moveTo(-hsize, -hsize);
3304 result.lineTo(hsize, hsize);
3305 result.moveTo(-hsize, hsize);
3306 result.lineTo(hsize, -hsize);
3307 break;
3308 case MarkerType::Triangle:
3309 result.moveTo(0, 0.6 * hsize);
3310 result.lineTo(-hsize, 0.6 * hsize);
3311 result.lineTo(0, -hsize);
3312 result.lineTo(hsize, 0.6 * hsize);
3313 result.closeSubPath();
3314 break;
3315 case MarkerType::Star: {
3316 double angle = M_PI / 2.0;
3317 for (int i = 0; i < 5; ++i) {
3318 double x = std::cos(angle) * hsize;
3319 double y = -std::sin(angle) * hsize;
3320 result.moveTo(0, 0);
3321 result.lineTo(x, y);
3322 angle += M_PI * 2.0 / 5.0;
3323 }
3324 }
3325 break;
3326 case MarkerType::InvertedTriangle:
3327 result.moveTo(0, -0.6 * hsize);
3328 result.lineTo(-hsize, -0.6 * hsize);
3329 result.lineTo(0, hsize);
3330 result.lineTo(hsize, -0.6 * hsize);
3331 result.closeSubPath();
3332 break;
3333 case MarkerType::Diamond: {
3334 double s = std::sqrt(2.0) * hsize;
3335 result.moveTo(0, s);
3336 result.lineTo(s, 0);
3337 result.lineTo(0, -s);
3338 result.lineTo(-s, 0);
3339 result.closeSubPath();
3340 break;
3341 }
3342 case MarkerType::Asterisk: {
3343 double angle = M_PI / 2.0;
3344 for (int i = 0; i < 6; ++i) {
3345 double x = std::cos(angle) * hsize;
3346 double y = -std::sin(angle) * hsize;
3347 result.moveTo(0, 0);
3348 result.lineTo(x, y);
3349 angle += M_PI / 3.0;
3350 }
3351 break;
3352 }
3353 case MarkerType::Custom:
3354 result = series.customMarker();
3355 break;
3356 default:
3357 ;
3358 }
3359 }
3360
renderLegendIcon(WPainter & painter,const WPointF & pos,const WDataSeries & series)3361 void WCartesianChart::renderLegendIcon(WPainter& painter,
3362 const WPointF& pos,
3363 const WDataSeries& series) const
3364 {
3365 WShadow shadow = painter.shadow();
3366 switch (series.type()) {
3367 case SeriesType::Bar: {
3368 WPainterPath path;
3369 path.moveTo(-6, 8);
3370 path.lineTo(-6, -8);
3371 path.lineTo(6, -8);
3372 path.lineTo(6, 8);
3373 painter.translate(pos.x() + 7.5, pos.y());
3374 painter.setShadow(series.shadow());
3375 painter.fillPath(path, series.brush());
3376 painter.setShadow(shadow);
3377 painter.strokePath(path, series.pen());
3378 painter.translate(-(pos.x() + 7.5), -pos.y());
3379 break;
3380 }
3381 case SeriesType::Line:
3382 case SeriesType::Curve: {
3383 #ifdef WT_TARGET_JAVA
3384 painter.setPen(WPen(series.pen()));
3385 #else
3386 painter.setPen(series.pen());
3387 #endif
3388 double offset = (series.pen().width() == 0 ? 0.5 : 0);
3389 painter.setShadow(series.shadow());
3390 painter.drawLine(pos.x(), pos.y() + offset, pos.x() + 16, pos.y() + offset);
3391 painter.setShadow(shadow);
3392 }
3393 // no break;
3394 case SeriesType::Point: {
3395 WPainterPath path;
3396 drawMarker(series, path);
3397 if (!path.isEmpty()) {
3398 painter.translate(pos.x() + 8, pos.y());
3399 painter.setShadow(series.shadow());
3400 painter.fillPath(path, series.markerBrush());
3401 painter.setShadow(shadow);
3402 painter.strokePath(path, series.markerPen());
3403 painter.translate(- (pos.x() + 8), -pos.y());
3404 }
3405
3406 break;
3407 }
3408 }
3409 }
3410
renderLegendItem(WPainter & painter,const WPointF & pos,const WDataSeries & series)3411 void WCartesianChart::renderLegendItem(WPainter& painter,
3412 const WPointF& pos,
3413 const WDataSeries& series) const
3414 {
3415 WPen fontPen = painter.pen();
3416
3417 renderLegendIcon(painter, pos, series);
3418
3419 #ifdef WT_TARGET_JAVA
3420 painter.setPen(WPen(fontPen));
3421 #else
3422 painter.setPen(fontPen);
3423 #endif
3424 int width = (int)legendColumnWidth().toPixels();
3425 if (width < 100)
3426 width = 100;
3427 painter.drawText(pos.x() + 23, pos.y() - 9, width, 20,
3428 WFlags<AlignmentFlag>(AlignmentFlag::Left) | AlignmentFlag::Middle,
3429 series.model()->headerData(series.modelColumn()));
3430 }
3431
prepareAxes(WPaintDevice * device)3432 bool WCartesianChart::prepareAxes(WPaintDevice *device) const
3433 {
3434 // No axes
3435 if (xAxes_.empty())
3436 return true;
3437
3438 if (yAxes_.empty())
3439 return true;
3440
3441 Orientation yDir = orientation_;
3442 Orientation xDir = orientation_
3443 == Orientation::Vertical ? Orientation::Horizontal : Orientation::Vertical;
3444
3445 for (std::size_t i = 0; i < xAxes_.size(); ++i)
3446 if (!xAxes_[i].axis->prepareRender(xDir, chartArea_.width()))
3447 return false;
3448
3449 for (std::size_t i = 0; i < yAxes_.size(); ++i)
3450 if (!yAxes_[i].axis->prepareRender(yDir, chartArea_.height()))
3451 return false;
3452
3453 for (std::size_t i = 0; i < xAxes_.size(); ++i)
3454 xAxes_[i].location.initLoc = xAxes_[i].axis->location();
3455
3456 std::vector<const WAxis*> minimumXaxes = collectAxesAtLocation(Axis::X, AxisValue::Minimum);
3457 int offset = 0;
3458 for (std::size_t i = 0; i < minimumXaxes.size(); ++i) {
3459 const WAxis &axis = *minimumXaxes[i];
3460 if (axis.location() != AxisValue::Both)
3461 xAxes_[axis.xAxisId()].location.initLoc = AxisValue::Minimum;
3462 xAxes_[axis.xAxisId()].location.minOffset = offset;
3463 xAxes_[axis.xAxisId()].calculatedWidth =
3464 calcAxisSize(axis, device) + 10;
3465 if (i == 0 && axis.tickDirection() == TickDirection::Inwards)
3466 offset += 10;
3467 else
3468 offset += xAxes_[axis.xAxisId()].calculatedWidth;
3469 }
3470
3471 std::vector<const WAxis*> maximumXaxes = collectAxesAtLocation(Axis::X, AxisValue::Maximum);
3472 offset = 0;
3473 for (std::size_t i = 0; i < maximumXaxes.size(); ++i) {
3474 const WAxis &axis = *maximumXaxes[i];
3475 if (axis.location() != AxisValue::Both)
3476 xAxes_[axis.xAxisId()].location.initLoc = AxisValue::Maximum;
3477 xAxes_[axis.xAxisId()].location.maxOffset = offset;
3478 xAxes_[axis.xAxisId()].calculatedWidth =
3479 calcAxisSize(axis, device) + 10;
3480 if (i == 0 && axis.tickDirection() == TickDirection::Inwards)
3481 offset += 10;
3482 else
3483 offset += xAxes_[axis.xAxisId()].calculatedWidth;
3484 }
3485
3486 for (int i = 0; i < xAxisCount(); ++i)
3487 xAxes_[i].location.finLoc = xAxes_[i].location.initLoc;
3488
3489 if (!minimumXaxes.empty() &&
3490 minimumXaxes[0]->location() == AxisValue::Minimum &&
3491 minimumXaxes[0]->scale() != AxisScale::Discrete &&
3492 (axis(Axis::Y).inverted() ?
3493 yAxes_[0].axis->segments_.back().renderMaximum == 0 :
3494 yAxes_[0].axis->segments_.front().renderMinimum == 0) &&
3495 minimumXaxes[0]->tickDirection() == TickDirection::Outwards) {
3496 xAxes_[minimumXaxes[0]->xAxisId()].location.finLoc = AxisValue::Zero;
3497 }
3498
3499 if (!maximumXaxes.empty() &&
3500 maximumXaxes[0]->location() == AxisValue::Maximum &&
3501 maximumXaxes[0]->scale() != AxisScale::Discrete &&
3502 (axis(Axis::Y).inverted() ?
3503 yAxes_[0].axis->segments_.front().renderMinimum == 0 :
3504 yAxes_[0].axis->segments_.back().renderMaximum == 0)) {
3505 xAxes_[maximumXaxes[0]->xAxisId()].location.finLoc = AxisValue::Zero;
3506 }
3507
3508 for (std::size_t i = 0; i < yAxes_.size(); ++i)
3509 yAxes_[i].location.initLoc = yAxes_[i].axis->location();
3510
3511 std::vector<const WAxis*> minimumYaxes = collectAxesAtLocation(Axis::Y, AxisValue::Minimum);
3512 offset = 0;
3513 for (std::size_t i = 0; i < minimumYaxes.size(); ++i) {
3514 const WAxis &axis = *minimumYaxes[i];
3515 if (axis.location() != AxisValue::Both)
3516 yAxes_[axis.yAxisId()].location.initLoc = AxisValue::Minimum;
3517 yAxes_[axis.yAxisId()].location.minOffset = offset;
3518 yAxes_[axis.yAxisId()].calculatedWidth =
3519 calcAxisSize(axis, device) + 10;
3520 if (i == 0 && axis.tickDirection() == TickDirection::Inwards)
3521 offset += 10;
3522 else
3523 offset += yAxes_[axis.yAxisId()].calculatedWidth;
3524 }
3525
3526 std::vector<const WAxis*> maximumYaxes = collectAxesAtLocation(Axis::Y, AxisValue::Maximum);
3527 offset = 0;
3528 for (std::size_t i = 0; i < maximumYaxes.size(); ++i) {
3529 const WAxis &axis = *maximumYaxes[i];
3530 if (axis.location() != AxisValue::Both)
3531 yAxes_[axis.yAxisId()].location.initLoc = AxisValue::Maximum;
3532 yAxes_[axis.yAxisId()].location.maxOffset = offset;
3533 yAxes_[axis.yAxisId()].calculatedWidth =
3534 calcAxisSize(axis, device) + 10;
3535 if (i == 0 && axis.tickDirection() == TickDirection::Inwards)
3536 offset += 10;
3537 else
3538 offset += yAxes_[axis.yAxisId()].calculatedWidth;
3539 }
3540
3541 for (int i = 0; i < yAxisCount(); ++i)
3542 yAxes_[i].location.finLoc = yAxes_[i].location.initLoc;
3543
3544 if (!minimumYaxes.empty() &&
3545 minimumYaxes[0]->location() == AxisValue::Minimum &&
3546 (axis(Axis::X).inverted() ?
3547 xAxes_[0].axis->segments_.back().renderMaximum == 0 :
3548 xAxes_[0].axis->segments_.front().renderMinimum == 0) &&
3549 minimumYaxes[0]->tickDirection() == TickDirection::Outwards) {
3550 yAxes_[minimumYaxes[0]->yAxisId()].location.finLoc = AxisValue::Zero;
3551 }
3552
3553 if (!maximumYaxes.empty() &&
3554 maximumYaxes[0]->location() == AxisValue::Maximum &&
3555 (axis(Axis::X).inverted() ?
3556 xAxes_[0].axis->segments_.front().renderMinimum == 0 :
3557 xAxes_[0].axis->segments_.back().renderMaximum == 0)) {
3558 yAxes_[maximumYaxes[0]->yAxisId()].location.finLoc = AxisValue::Zero;
3559 }
3560
3561 return true;
3562 }
3563
3564 // Collects Y axes on minimum or maximum side
3565 // The vector is ordered from innermost to outermost
3566 // NOTE: should be called after prepareRender is called for all axes!
collectAxesAtLocation(Axis ax,AxisValue side)3567 std::vector<const WAxis*> WCartesianChart::collectAxesAtLocation(Axis ax, AxisValue side) const
3568 {
3569 const std::vector<AxisStruct> &axes = ax == Axis::X ? xAxes_ : yAxes_;
3570 const std::vector<AxisStruct> &otherAxes = ax == Axis::X ? yAxes_ : xAxes_;
3571
3572 std::vector<const WAxis*> result;
3573
3574 // Innermost axes are axes set to location ZeroValue
3575 // and there is no 0 on the X axis
3576 for (std::size_t i = 0; i < axes.size(); ++i) {
3577 const WAxis &axis = *axes[i].axis;
3578 if (!axis.isVisible())
3579 continue;
3580 // For ZeroValue: look at the first of the other axes
3581 if (axis.location() == AxisValue::Zero) {
3582 if (side == AxisValue::Minimum) {
3583 if (axis.scale() == AxisScale::Discrete ||
3584 otherAxes[0].axis->segments_.front().renderMinimum >= 0 ||
3585 (!otherAxes[0].axis->isOnAxis(0.0) &&
3586 otherAxes[0].axis->segments_.back().renderMaximum > 0))
3587 result.push_back(&axis);
3588 } else if (side == AxisValue::Maximum) {
3589 if (otherAxes[0].axis->segments_.back().renderMaximum <= 0)
3590 result.push_back(&axis);
3591 }
3592 }
3593 }
3594
3595 for (std::size_t i = 0; i < axes.size(); ++i) {
3596 const WAxis &axis = *axes[i].axis;
3597 if (!axis.isVisible())
3598 continue;
3599 if (axis.location() == side || axis.location() == AxisValue::Both)
3600 result.push_back(&axis);
3601 }
3602
3603 return result;
3604 }
3605
calcAxisSize(const WAxis & axis,WPaintDevice * device)3606 int WCartesianChart::calcAxisSize(const WAxis &axis, WPaintDevice *device) const
3607 {
3608 if (device->features().test(PaintDeviceFeatureFlag::FontMetrics)) {
3609 // TICK_LENGTH + axis labels + (optional) title
3610 if ((orientation() == Orientation::Horizontal) != /*XOR*/ (axis.id() == Axis::X)) {
3611 WMeasurePaintDevice md(device);
3612 double h = TICK_LENGTH;
3613 h += axis.calcMaxTickLabelSize(&md, Orientation::Vertical);
3614 if (!axis.title().empty()) {
3615 h += axis.calcTitleSize(&md, Orientation::Vertical);
3616 }
3617 return static_cast<int>(std::ceil(h));
3618 } else {
3619 WMeasurePaintDevice md(device);
3620 double w = TICK_LENGTH;
3621 w += axis.calcMaxTickLabelSize(&md, Orientation::Horizontal);
3622 if (!axis.title().empty() &&
3623 axis.titleOrientation() == Orientation::Vertical) {
3624 w += axis.calcTitleSize(&md, Orientation::Vertical) + 10;
3625 }
3626 return static_cast<int>(std::ceil(w));
3627 }
3628 } else {
3629 if ((orientation() == Orientation::Horizontal) != /*XOR*/ (axis.id() == Axis::X)) {
3630 return TICK_LENGTH + 20 + (axis.title().empty() ? 0 : 20);
3631 } else {
3632 return TICK_LENGTH + 30 + (axis.title().empty() ? 0 : 15);
3633 }
3634 }
3635 }
3636
map(double xValue,double yValue,Axis yAxis,int currentXSegment,int currentYSegment)3637 WPointF WCartesianChart::map(double xValue, double yValue,
3638 Axis yAxis, int currentXSegment,
3639 int currentYSegment) const
3640 {
3641 return map(xValue, yValue, yAxis == Axis::Y1 ? 0 : 1, currentXSegment, currentYSegment);
3642 }
3643
map(double xValue,double yValue,int yAxis,int currentXSegment,int currentYSegment)3644 WPointF WCartesianChart::map(double xValue, double yValue,
3645 int yAxis, int currentXSegment,
3646 int currentYSegment) const
3647 {
3648 return map(xValue, yValue, xAxis(0), this->yAxis(yAxis), currentXSegment, currentYSegment);
3649 }
3650
map(double xValue,double yValue,const WAxis & xAxis,const WAxis & yAxis,int currentXSegment,int currentYSegment)3651 WPointF WCartesianChart::map(double xValue, double yValue,
3652 const WAxis &xAxis, const WAxis &yAxis,
3653 int currentXSegment, int currentYSegment) const
3654 {
3655 double x = chartArea_.left() + xAxis.mapToDevice(xValue, currentXSegment);
3656 double y = chartArea_.bottom() - yAxis.mapToDevice(yValue, currentYSegment);
3657
3658 return WPointF(x, y);
3659 }
3660
calcChartArea()3661 void WCartesianChart::calcChartArea() const
3662 {
3663 if (orientation_ == Orientation::Vertical)
3664 chartArea_ = WRectF(plotAreaPadding(Side::Left),
3665 plotAreaPadding(Side::Top),
3666 std::max(10, width_ - plotAreaPadding(Side::Left)
3667 - plotAreaPadding(Side::Right)),
3668 std::max(10, height_ - plotAreaPadding(Side::Top)
3669 - plotAreaPadding(Side::Bottom)));
3670 else
3671 chartArea_ = WRectF(plotAreaPadding(Side::Top),
3672 plotAreaPadding(Side::Right),
3673 std::max(10, width_ - plotAreaPadding(Side::Top)
3674 - plotAreaPadding(Side::Bottom)),
3675 std::max(10, height_ - plotAreaPadding(Side::Right)
3676 - plotAreaPadding(Side::Left)));
3677 }
3678
chartSegmentArea(const WAxis & yAxis,int xSegment,int ySegment)3679 WRectF WCartesianChart::chartSegmentArea(const WAxis &yAxis,
3680 int xSegment,
3681 int ySegment) const
3682 {
3683 return chartSegmentArea(xAxis(0), yAxis, xSegment, ySegment);
3684 }
3685
chartSegmentArea(const WAxis & xAxis,const WAxis & yAxis,int xSegment,int ySegment)3686 WRectF WCartesianChart::chartSegmentArea(const WAxis& xAxis,
3687 const WAxis& yAxis,
3688 int xSegment,
3689 int ySegment) const
3690 {
3691 const WAxis::Segment& xs = xAxis.segments_[xSegment];
3692 const WAxis::Segment& ys = yAxis.segments_[ySegment];
3693
3694 // margin used when clipping, see also WAxis::prepareRender(),
3695 // when the renderMinimum/maximum is 0, clipping is done exact
3696
3697 double xRenderStart = xAxis.inverted() ? xAxis.mapToDevice(xs.renderMaximum, xSegment) : xs.renderStart;
3698 double xRenderEnd = xAxis.inverted() ? xAxis.mapToDevice(xs.renderMinimum, xSegment) : xs.renderStart + xs.renderLength;
3699 double yRenderStart = yAxis.inverted() ? yAxis.mapToDevice(ys.renderMaximum, ySegment) : ys.renderStart;
3700 double yRenderEnd = yAxis.inverted() ? yAxis.mapToDevice(ys.renderMinimum, ySegment) : ys.renderStart + ys.renderLength;
3701
3702 double x1 = chartArea_.left() + xRenderStart
3703 + (xSegment == 0
3704 ? (xs.renderMinimum == 0 ? 0 : -axisPadding())
3705 : -xAxis.segmentMargin() / 2);
3706 double x2 = chartArea_.left() + xRenderEnd
3707 + (xSegment == xAxis.segmentCount() - 1
3708 ? (xs.renderMaximum == 0 ? 0 : axisPadding())
3709 : xAxis.segmentMargin() / 2);
3710
3711 double y1 = chartArea_.bottom() - yRenderEnd
3712 - (ySegment == yAxis.segmentCount() - 1
3713 ? (ys.renderMaximum == 0 ? 0 : axisPadding())
3714 : yAxis.segmentMargin() / 2);
3715 double y2 = chartArea_.bottom() - yRenderStart
3716 + (ySegment == 0
3717 ? (ys.renderMinimum == 0 ? 0 : axisPadding())
3718 : yAxis.segmentMargin() / 2);
3719
3720 return WRectF(std::floor(x1 + 0.5), std::floor(y1 + 0.5),
3721 std::floor(x2 - x1), std::floor(y2 - y1));
3722 }
3723
renderBackground(WPainter & painter)3724 void WCartesianChart::renderBackground(WPainter& painter) const
3725 {
3726 if (background().style() != BrushStyle::None)
3727 painter.fillRect(hv(chartArea_), background());
3728
3729 if (onDemandLoadingEnabled() && xAxisCount() == 1) {
3730 // TODO(Roel): can't do background properly when there are multiple X axes with on demand loading?
3731 painter.save();
3732 WPainterPath clipPath;
3733 clipPath.addRect(hv(chartArea_));
3734 painter.setClipPath(clipPath);
3735 painter.setClipping(true);
3736
3737 double zoomRange = xAxis(0).zoomMaximum() - xAxis(0).zoomMinimum();
3738 double zoomStart = xAxis(0).zoomMinimum() - zoomRange;
3739 double zoomEnd = xAxis(0).zoomMaximum() + zoomRange;
3740 double minX = std::max(chartArea_.left() + xAxis(0).mapToDevice(zoomStart), chartArea_.left());
3741 double maxX = std::min(chartArea_.left() + xAxis(0).mapToDevice(zoomEnd), chartArea_.right());
3742 painter.fillRect(zoomRangeTransform(xAxis(0), yAxis(0)).map(hv(WRectF(chartArea_.left(), chartArea_.top(), minX - chartArea_.left(), chartArea_.height()))), loadingBackground());
3743 painter.fillRect(zoomRangeTransform(xAxis(0), yAxis(0)).map(hv(WRectF(maxX, chartArea_.top(), chartArea_.right() - maxX, chartArea_.height()))), loadingBackground());
3744
3745 painter.restore();
3746 }
3747 }
3748
renderGrid(WPainter & painter,const WAxis & ax)3749 void WCartesianChart::renderGrid(WPainter& painter, const WAxis& ax) const
3750 {
3751 if (!ax.isGridLinesEnabled())
3752 return;
3753
3754 const bool isXAxis = ax.id() == Axis::X;
3755 const bool isYAxis = ax.id() != Axis::X;
3756
3757 if (!isXAxis && xAxes_.empty())
3758 return;
3759
3760 if (!isYAxis && yAxes_.empty())
3761 return;
3762
3763 const WAxis& other = isYAxis ? axis(Axis::X) : axis(Axis::Y1);
3764 const WAxis::Segment& s0 = other.segments_.front();
3765 const WAxis::Segment& sn = other.segments_.back();
3766
3767 double ou0 = s0.renderStart;
3768 double oun = sn.renderStart + sn.renderLength;
3769
3770 // Adjust for potentially different axis padding on other Y axes
3771 if (!isYAxis) {
3772 bool gridLines = axis(Axis::Y1).isGridLinesEnabled();
3773 for (int i = 1; i < yAxisCount(); ++i) {
3774 if (!(yAxis(i).isVisible() && yAxis(i).isGridLinesEnabled()))
3775 continue;
3776 const WAxis::Segment &s0_2 = yAxis(i).segments_.front();
3777 const WAxis::Segment &sn_2 = yAxis(i).segments_.front();
3778 if (!gridLines || s0_2.renderStart < ou0)
3779 ou0 = s0_2.renderStart;
3780 if (!gridLines || sn_2.renderStart + sn_2.renderLength > oun)
3781 oun = sn_2.renderStart + sn_2.renderLength;
3782 gridLines = true;
3783 }
3784 }
3785
3786 if (!isYAxis) {
3787 ou0 = chartArea_.bottom() - ou0;
3788 oun = chartArea_.bottom() - oun;
3789 } else {
3790 ou0 = chartArea_.left() + ou0;
3791 oun = chartArea_.left() + oun;
3792 }
3793
3794 if (isInteractive()) {
3795 painter.save();
3796 WPainterPath clipPath;
3797 clipPath.addRect(hv(chartArea_));
3798 painter.setClipPath(clipPath);
3799 painter.setClipping(true);
3800 }
3801
3802 std::vector<WPen> pens;
3803 const std::vector<PenAssignment> &assignments =
3804 ax.id() == Axis::X ? xAxes_[ax.xAxisId()].pens : yAxes_[ax.yAxisId()].pens;
3805 if (assignments.empty()) {
3806 pens.push_back(ax.gridLinesPen());
3807 } else {
3808 for (std::size_t i = 0; i < assignments.size(); ++i) {
3809 pens.push_back(assignments[i].gridPen.value());
3810 }
3811 }
3812
3813 AxisConfig axisConfig;
3814 if (ax.location() == AxisValue::Both)
3815 axisConfig.side = AxisValue::Minimum;
3816 else
3817 axisConfig.side = ax.location();
3818
3819 for (unsigned level = 1; level <= pens.size(); ++level) {
3820 WPainterPath gridPath;
3821
3822 axisConfig.zoomLevel = level;
3823 std::vector<double> gridPos = ax.gridLinePositions(axisConfig);
3824
3825 for (unsigned i = 0; i < gridPos.size(); ++i) {
3826 double u = gridPos[i];
3827
3828 if (isYAxis) {
3829 u = chartArea_.bottom() - u;
3830 gridPath.moveTo(hv(ou0, u));
3831 gridPath.lineTo(hv(oun, u));
3832 } else {
3833 u = chartArea_.left() + u;
3834 gridPath.moveTo(hv(u, ou0));
3835 gridPath.lineTo(hv(u, oun));
3836 }
3837 }
3838
3839 painter.strokePath(zoomRangeTransform(isYAxis ? ax.yAxisId() : 0).map(gridPath).crisp(), pens[level - 1]);
3840 }
3841
3842 if (isInteractive()) {
3843 painter.restore();
3844 }
3845 }
3846
renderAxis(WPainter & painter,const WAxis & axis,WFlags<AxisProperty> properties)3847 void WCartesianChart::renderAxis(WPainter& painter, const WAxis& axis,
3848 WFlags<AxisProperty> properties) const
3849 {
3850 if (!axis.isVisible())
3851 return;
3852
3853 bool isYAxis = axis.id() != Axis::X;
3854
3855 const AxisValue location = axis.id() == Axis::X ? xAxes_[axis.xAxisId()].location.finLoc : yAxes_[axis.yAxisId()].location.finLoc;
3856
3857 if (isInteractive() && dynamic_cast<WCanvasPaintDevice*>(painter.device())) {
3858 WRectF clipRect;
3859 WRectF area = hv(chartArea_);
3860 if (axis.location() == AxisValue::Zero && location == AxisValue::Zero) {
3861 clipRect = area;
3862 } else if (isYAxis != /*XOR*/ (orientation() == Orientation::Horizontal)) {
3863 double h = area.height();
3864 if ((xAxes_[0].location.finLoc == AxisValue::Zero &&
3865 orientation() == Orientation::Vertical) ||
3866 (yAxes_[0].location.finLoc == AxisValue::Zero &&
3867 orientation() == Orientation::Horizontal)) {
3868 h += 1; // prevent clipping off of zero tick
3869 }
3870 clipRect = WRectF(0.0, area.top(), isYAxis ? width_ : height_, h);
3871 } else {
3872 clipRect = WRectF(area.left(), 0.0, area.width(), isYAxis ? height_ : width_);
3873 }
3874 if (properties == AxisProperty::Labels) {
3875 clipRect = WRectF(clipRect.left() - 1, clipRect.top() - 1,
3876 clipRect.width() + 2, clipRect.height() + 2);
3877 }
3878 WPainterPath clipPath;
3879 clipPath.addRect(clipRect);
3880 painter.save();
3881 painter.setClipPath(clipPath);
3882 painter.setClipping(true);
3883 }
3884
3885 std::vector<AxisValue> locations;
3886 if (location == AxisValue::Both) {
3887 locations.push_back(AxisValue::Minimum);
3888 locations.push_back(AxisValue::Maximum);
3889 } else
3890 locations.push_back(location);
3891
3892 for (std::size_t l = 0; l < locations.size(); ++l) {
3893 WPointF axisStart, axisEnd;
3894 double tickStart = 0.0, tickEnd = 0.0, labelPos = 0.0;
3895 AlignmentFlag labelHFlag = AlignmentFlag::Center,
3896 labelVFlag = AlignmentFlag::Middle;
3897
3898 if (isYAxis) {
3899 labelVFlag = AlignmentFlag::Middle;
3900 axisStart.setY(chartArea_.bottom());
3901 axisEnd.setY(chartArea_.top());
3902 } else {
3903 labelHFlag = AlignmentFlag::Center;
3904 axisStart.setX(chartArea_.left());
3905 axisEnd.setX(chartArea_.right());
3906 }
3907
3908 switch (locations[l]) {
3909 case AxisValue::Minimum:
3910 if (isYAxis) {
3911 double x = chartArea_.left() - yAxes_[axis.yAxisId()].location.minOffset;
3912 if (axis.tickDirection() == TickDirection::Inwards) {
3913 tickStart = 0;
3914 tickEnd = TICK_LENGTH;
3915 labelPos = TICK_LENGTH;
3916 labelHFlag = AlignmentFlag::Left;
3917
3918 axisStart.setX(x);
3919 axisEnd.setX(x);
3920 } else {
3921 tickStart = -TICK_LENGTH;
3922 tickEnd = 0;
3923 labelPos = -TICK_LENGTH;
3924 labelHFlag = AlignmentFlag::Right;
3925
3926 x -= axis.margin();
3927 axisStart.setX(x);
3928 axisEnd.setX(x);
3929 }
3930 } else {
3931 double y = chartArea_.bottom() - 1 + xAxes_[axis.xAxisId()].location.minOffset;
3932 if (axis.tickDirection() == TickDirection::Inwards) {
3933 tickStart = -TICK_LENGTH;
3934 tickEnd = 0;
3935 labelPos = -TICK_LENGTH;
3936 labelVFlag = AlignmentFlag::Bottom;
3937
3938 axisStart.setY(y);
3939 axisEnd.setY(y);
3940 } else {
3941 tickStart = 0;
3942 tickEnd = TICK_LENGTH;
3943 labelPos = TICK_LENGTH;
3944 labelVFlag = AlignmentFlag::Top;
3945
3946 axisStart.setY(y);
3947 axisEnd.setY(y);
3948 }
3949 }
3950
3951 break;
3952 case AxisValue::Maximum:
3953 if (isYAxis) {
3954 double x = chartArea_.right() + yAxes_[axis.yAxisId()].location.maxOffset;
3955 if (axis.tickDirection() == TickDirection::Inwards) {
3956 tickStart = -TICK_LENGTH;
3957 tickEnd = 0;
3958 labelPos = -TICK_LENGTH;
3959 labelHFlag = AlignmentFlag::Right;
3960
3961 x -= 1;
3962 axisStart.setX(x);
3963 axisEnd.setX(x);
3964 } else {
3965 tickStart = 0;
3966 tickEnd = TICK_LENGTH;
3967 labelPos = TICK_LENGTH;
3968 labelHFlag = AlignmentFlag::Left;
3969
3970 x += axis.margin();
3971 axisStart.setX(x);
3972 axisEnd.setX(x);
3973 }
3974 } else {
3975 double y = chartArea_.top() - xAxes_[axis.xAxisId()].location.maxOffset;
3976 if (axis.tickDirection() == TickDirection::Inwards) {
3977 tickStart = 0;
3978 tickEnd = TICK_LENGTH;
3979 labelPos = TICK_LENGTH;
3980 labelVFlag = AlignmentFlag::Top;
3981
3982 axisStart.setY(y);
3983 axisEnd.setY(y);
3984 } else {
3985 tickStart = -TICK_LENGTH;
3986 tickEnd = 0;
3987 labelPos = -TICK_LENGTH;
3988 labelVFlag = AlignmentFlag::Bottom;
3989
3990 axisStart.setY(y);
3991 axisEnd.setY(y);
3992 }
3993 }
3994
3995 break;
3996 case AxisValue::Zero:
3997 tickStart = -TICK_LENGTH;
3998 tickEnd = TICK_LENGTH;
3999
4000 if (isYAxis) {
4001 double x = chartArea_.left() + this->axis(Axis::X).mapToDevice(0.0);
4002 axisStart.setX(x);
4003 axisEnd.setX(x);
4004
4005 labelHFlag = AlignmentFlag::Right;
4006
4007 /* force labels left even if axis is in middle */
4008 if (type() == ChartType::Category)
4009 labelPos = chartArea_.left() - axisStart.x() - TICK_LENGTH;
4010 else
4011 labelPos = -TICK_LENGTH;
4012 } else {
4013 double y = chartArea_.bottom() - this->axis(Axis::Y).mapToDevice(0.0);
4014 axisStart.setY(y);
4015 axisEnd.setY(y);
4016
4017 labelVFlag = AlignmentFlag::Top;
4018
4019 /* force labels bottom even if axis is in middle */
4020 if (type() == ChartType::Category)
4021 labelPos = chartArea_.bottom() - axisStart.y() + TICK_LENGTH;
4022 else
4023 labelPos = TICK_LENGTH;
4024 }
4025
4026 break;
4027 case AxisValue::Both:
4028 assert(false);
4029 break;
4030 }
4031
4032 if (properties.test(AxisProperty::Labels) && !axis.title().empty()) {
4033 if (isInteractive()) painter.setClipping(false);
4034
4035 WFont oldFont2 = painter.font();
4036 WFont titleFont = axis.titleFont();
4037 painter.setFont(titleFont);
4038
4039 bool chartVertical = orientation() == Orientation::Vertical;
4040
4041 if (isYAxis) {
4042 /* Y Axes */
4043 double u = axisStart.x();
4044 if (chartVertical) {
4045 if(axis.titleOrientation() == Orientation::Horizontal) {
4046 renderLabel
4047 (painter, axis.title(),
4048 WPointF(u + (labelHFlag == AlignmentFlag::Right ? 15 : -15),
4049 chartArea_.top() - 8),
4050 WFlags<AlignmentFlag>(labelHFlag) | AlignmentFlag::Bottom, 0, 10);
4051 } else {
4052 WPaintDevice *device = painter.device();
4053 double size = 0, titleSizeW = 0;
4054 if (device->features().test(PaintDeviceFeatureFlag::FontMetrics)) {
4055 if (axis.tickDirection() == TickDirection::Outwards)
4056 size = axis.calcMaxTickLabelSize(device, Orientation::Horizontal);
4057 titleSizeW = axis.calcTitleSize(device, Orientation::Vertical);
4058 if (axis.tickDirection() == TickDirection::Inwards)
4059 titleSizeW = -titleSizeW;
4060 } else {
4061 size = 35;
4062 if (axis.tickDirection() == TickDirection::Inwards)
4063 size = -20;
4064 }
4065
4066 renderLabel(painter, axis.title(),
4067 WPointF(u + (labelHFlag == AlignmentFlag::Right
4068 ? -( size + titleSizeW + 5)
4069 : +( size + titleSizeW + 5)),
4070 chartArea_.center().y()),
4071 WFlags<AlignmentFlag>(AlignmentFlag::Center) | AlignmentFlag::Middle,
4072 locations[l] == AxisValue::Maximum ? -90 : 90, 10);
4073 }
4074 } else {
4075 double extraMargin = 0;
4076 WPaintDevice *device = painter.device();
4077 if (axis.tickDirection() == TickDirection::Outwards) {
4078 if (device->features().test(PaintDeviceFeatureFlag::FontMetrics))
4079 extraMargin = axis.calcMaxTickLabelSize(device, Orientation::Vertical);
4080 else
4081 extraMargin = 15;
4082 }
4083 if (locations[l] != AxisValue::Maximum)
4084 extraMargin = -extraMargin;
4085 WFlags<AlignmentFlag> alignment = WFlags<AlignmentFlag>(locations[l] == AxisValue::Maximum ? AlignmentFlag::Left : AlignmentFlag::Right) | AlignmentFlag::Middle;
4086 renderLabel(painter, axis.title(),
4087 WPointF(u + extraMargin, chartArea_.center().y()),
4088 alignment, 0, 10);
4089 }
4090 } else {
4091 /* X Axes */
4092 double u = axisStart.y();
4093 if (chartVertical) {
4094 double extraMargin = 0;
4095 WPaintDevice *device = painter.device();
4096 if (device->features().test(PaintDeviceFeatureFlag::FontMetrics)) {
4097 if (axis.tickDirection() == TickDirection::Outwards)
4098 extraMargin = axis.calcMaxTickLabelSize(device,
4099 Orientation::Vertical);
4100 } else {
4101 if (axis.tickDirection() == TickDirection::Outwards)
4102 extraMargin = 15;
4103 }
4104 if (locations[l] == AxisValue::Maximum)
4105 extraMargin = -extraMargin;
4106 WFlags<AlignmentFlag> alignment =
4107 WFlags<AlignmentFlag>(locations[l] == AxisValue::Maximum ?
4108 AlignmentFlag::Bottom : AlignmentFlag::Top) |
4109 AlignmentFlag::Center;
4110 renderLabel(painter, axis.title(),
4111 WPointF(chartArea_.center().x(), u + extraMargin),
4112 alignment, 0, 10);
4113 } else {
4114 if (axis.titleOrientation() == Orientation::Vertical) {
4115 // Orientation::Vertical X axis
4116 WPaintDevice *device = painter.device();
4117 double extraMargin = 0;
4118 if (device->features().test(PaintDeviceFeatureFlag::FontMetrics)) {
4119 if (axis.tickDirection() == TickDirection::Outwards)
4120 extraMargin = axis.calcMaxTickLabelSize(device,
4121 Orientation::Horizontal);
4122 extraMargin += axis.calcTitleSize(device, Orientation::Vertical);
4123 } else {
4124 extraMargin = 40;
4125 }
4126 if (locations[l] == AxisValue::Maximum)
4127 extraMargin = -extraMargin;
4128
4129 renderLabel(painter, axis.title(),
4130 WPointF(chartArea_.center().x(), u + extraMargin),
4131 WFlags<AlignmentFlag>(AlignmentFlag::Middle) | AlignmentFlag::Center,
4132 locations[l] == AxisValue::Maximum ? -90 : 90, 10);
4133 } else {
4134 WFlags<AlignmentFlag> alignment =
4135 WFlags<AlignmentFlag>(locations[l] == AxisValue::Maximum ?
4136 AlignmentFlag::Bottom : AlignmentFlag::Top) |
4137 AlignmentFlag::Left;
4138 renderLabel(painter, axis.title(), WPointF(chartArea_.right(), u),
4139 alignment, 0, 8);
4140 }
4141 }
4142 }
4143
4144 painter.setFont(oldFont2);
4145
4146 if (isInteractive())
4147 painter.setClipping(true);
4148 }
4149
4150 const double ANGLE1 = 15;
4151 const double ANGLE2 = 80;
4152
4153 /* Adjust alignment when rotating the labels */
4154 if (isYAxis) {
4155 if (axis.labelAngle() > ANGLE1) {
4156 labelVFlag =
4157 labelPos < 0 ? AlignmentFlag::Bottom : AlignmentFlag::Top;
4158 if (axis.labelAngle() > ANGLE2)
4159 labelHFlag = AlignmentFlag::Center;
4160 } else if (axis.labelAngle() < -ANGLE1) {
4161 labelVFlag =
4162 labelPos < 0 ? AlignmentFlag::Top : AlignmentFlag::Bottom;
4163 if (axis.labelAngle() < -ANGLE2)
4164 labelHFlag = AlignmentFlag::Center;
4165 }
4166 } else {
4167 if (axis.labelAngle() > ANGLE1) {
4168 labelHFlag =
4169 labelPos > 0 ? AlignmentFlag::Right : AlignmentFlag::Left;
4170 if (axis.labelAngle() > ANGLE2)
4171 labelVFlag = AlignmentFlag::Middle;
4172 } else if (axis.labelAngle() < -ANGLE1) {
4173 labelHFlag =
4174 labelPos > 0 ? AlignmentFlag::Left : AlignmentFlag::Right;
4175 if (axis.labelAngle() < -ANGLE2)
4176 labelVFlag = AlignmentFlag::Middle;
4177 }
4178 }
4179
4180 /* perform hv() if necessary */
4181 if (orientation() == Orientation::Horizontal) {
4182 axisStart = hv(axisStart);
4183 axisEnd = hv(axisEnd);
4184
4185 AlignmentFlag rHFlag = AlignmentFlag::Center,
4186 rVFlag = AlignmentFlag::Middle;
4187
4188 switch (labelHFlag) {
4189 case AlignmentFlag::Left: rVFlag = AlignmentFlag::Top; break;
4190 case AlignmentFlag::Center: rVFlag = AlignmentFlag::Middle; break;
4191 case AlignmentFlag::Right: rVFlag = AlignmentFlag::Bottom; break;
4192 default: break;
4193 }
4194
4195 switch (labelVFlag) {
4196 case AlignmentFlag::Top: rHFlag = AlignmentFlag::Right; break;
4197 case AlignmentFlag::Middle: rHFlag = AlignmentFlag::Center; break;
4198 case AlignmentFlag::Bottom: rHFlag = AlignmentFlag::Left; break;
4199 default: break;
4200 }
4201
4202 labelHFlag = rHFlag;
4203 labelVFlag = rVFlag;
4204
4205 bool invertTicks = !isYAxis;
4206 if (invertTicks) {
4207 tickStart = -tickStart;
4208 tickEnd = -tickEnd;
4209 labelPos = -labelPos;
4210 }
4211 }
4212
4213 std::vector<WPen> pens;
4214 std::vector<WPen> textPens;
4215 if (isInteractive()) {
4216 const std::vector<PenAssignment> &assignment =
4217 axis.id() == Axis::X ? xAxes_[axis.xAxisId()].pens : yAxes_[axis.yAxisId()].pens;
4218 if (!assignment.empty()) {
4219 for (std::size_t i = 0; i < assignment.size(); ++i) {
4220 pens.push_back(assignment[i].pen.value());
4221 textPens.push_back(assignment[i].textPen.value());
4222 }
4223 }
4224 }
4225
4226 WTransform transform;
4227 WRectF area = hv(chartArea_);
4228 if (axis.location() == AxisValue::Zero) {
4229 transform =
4230 WTransform(1,0,0,-1,area.left(),area.bottom()) *
4231 xAxes_[axis.xAxisId()].transform * yAxes_[axis.yAxisId()].transform *
4232 WTransform(1,0,0,-1,-area.left(),area.bottom());
4233 } else if (isYAxis && orientation() == Orientation::Vertical) {
4234 transform = WTransform(1,0,0,-1,0,area.bottom()) * yAxes_[axis.yAxisId()].transform * WTransform(1,0,0,-1,0,area.bottom());
4235 } else if (isYAxis && orientation() == Orientation::Horizontal) {
4236 transform = WTransform(0,1,1,0,area.left(),0) * yAxes_[axis.yAxisId()].transform * WTransform(0,1,1,0,0,-area.left());
4237 } else if (orientation() == Orientation::Horizontal) {
4238 transform = WTransform(0,1,1,0,0,area.top()) * xAxes_[axis.xAxisId()].transform * WTransform(0,1,1,0,-area.top(),0);
4239 } else {
4240 transform = WTransform(1,0,0,1,area.left(),0) * xAxes_[axis.xAxisId()].transform * WTransform(1,0,0,1,-area.left(),0);
4241 }
4242
4243 AxisValue side = location == AxisValue::Both ? locations[l] : axis.location();
4244
4245 axis.render(painter, properties, axisStart, axisEnd, tickStart, tickEnd,
4246 labelPos, WFlags<AlignmentFlag>(labelHFlag) | labelVFlag, transform,
4247 side, pens, textPens);
4248 }
4249
4250 if (isInteractive()) {
4251 painter.restore();
4252 }
4253 }
4254
renderAxes(WPainter & painter,WFlags<AxisProperty> properties)4255 void WCartesianChart::renderAxes(WPainter& painter,
4256 WFlags<AxisProperty> properties) const
4257 {
4258 for (std::size_t i = 0; i < xAxes_.size(); ++i)
4259 renderAxis(painter, *xAxes_[i].axis, properties);
4260 for (std::size_t i = 0; i < yAxes_.size(); ++i)
4261 renderAxis(painter, *yAxes_[i].axis, properties);
4262 }
4263
hasInwardsXAxisOnMinimumSide()4264 bool WCartesianChart::hasInwardsXAxisOnMinimumSide() const
4265 {
4266 std::vector<const WAxis*> minimumXaxes = collectAxesAtLocation(Axis::X, AxisValue::Minimum);
4267 return !minimumXaxes.empty() && minimumXaxes[0]->tickDirection() == TickDirection::Inwards;
4268 }
4269
4270 // Determines if the first Y axis on the maximum side
4271 // is visible and has its tick direction inwards
4272 //
4273 // This is used to determine whether the border should
4274 // be shifted by one pixel in renderBorder()
hasInwardsYAxisOnMaximumSide()4275 bool WCartesianChart::hasInwardsYAxisOnMaximumSide() const
4276 {
4277 std::vector<const WAxis*> maximumYaxes = collectAxesAtLocation(Axis::Y, AxisValue::Maximum);
4278 return !maximumYaxes.empty() && maximumYaxes[0]->tickDirection() == TickDirection::Inwards;
4279 }
4280
renderBorder(WPainter & painter)4281 void WCartesianChart::renderBorder(WPainter& painter) const
4282 {
4283 WPainterPath area;
4284 int horizontalShift = 0,
4285 verticalShift = 0;
4286
4287 if (hasInwardsYAxisOnMaximumSide())
4288 horizontalShift = -1;
4289 if (hasInwardsXAxisOnMinimumSide())
4290 verticalShift = -1;
4291
4292 area.addRect(hv(WRectF(chartArea_.left(), chartArea_.top(),
4293 chartArea_.width() + horizontalShift,
4294 chartArea_.height() + verticalShift)));
4295 painter.strokePath(area.crisp(), borderPen_);
4296 }
4297
renderCurveLabels(WPainter & painter)4298 void WCartesianChart::renderCurveLabels(WPainter &painter) const
4299 {
4300 if (isInteractive()) {
4301 painter.save();
4302 WPainterPath clipPath;
4303 clipPath.addRect(hv(chartArea_));
4304 painter.setClipPath(clipPath);
4305 painter.setClipping(true);
4306 }
4307 for (std::size_t i = 0; i < curveLabels_.size(); ++i) {
4308 const CurveLabel &label = curveLabels_[i];
4309 for (std::size_t j = 0; j < series_.size(); ++j) {
4310 const WDataSeries &series = *series_[j];
4311 // Don't draw curve labels for hidden series
4312 if (series.isHidden())
4313 continue;
4314 if (&series == &label.series()) {
4315 WTransform t = zoomRangeTransform(series.yAxis());
4316 if (series.type() == SeriesType::Line || series.type() == SeriesType::Curve) {
4317 t = t * curveTransform(series);
4318 }
4319
4320 // Find the right x and y segment
4321 int xSegment = 0;
4322 double x = axis(Axis::X).getValue(label.x());
4323 if (!isInteractive())
4324 while (xSegment < axis(Axis::X).segmentCount() &&
4325 (axis(Axis::X).segments_[xSegment].renderMinimum > x ||
4326 axis(Axis::X).segments_[xSegment].renderMaximum < x)) {
4327 ++xSegment;
4328 }
4329
4330 int ySegment = 0;
4331 double y = yAxis(series.yAxis()).getValue(label.y());
4332 if (!isInteractive())
4333 while (ySegment < yAxis(series.yAxis()).segmentCount() &&
4334 (yAxis(series.yAxis()).segments_[ySegment].renderMinimum > y ||
4335 yAxis(series.yAxis()).segments_[ySegment].renderMaximum < y)) {
4336 ++ySegment;
4337 }
4338
4339 // Only draw the label if it is actually on a segment
4340 if (xSegment < axis(Axis::X).segmentCount() &&
4341 ySegment < yAxis(series.yAxis()).segmentCount()) {
4342 // Figure out the device coordinates of the point to draw a label at.
4343 WPointF devicePoint =
4344 mapToDeviceWithoutTransform(label.x(), label.y(),
4345 series.yAxis(), xSegment, ySegment);
4346 WTransform translation = WTransform().translate(t.map(devicePoint));
4347 painter.save();
4348 painter.setWorldTransform(translation);
4349
4350 label.render(painter);
4351
4352 painter.restore();
4353 }
4354 }
4355 }
4356 }
4357 if (isInteractive()) {
4358 painter.restore();
4359 }
4360 }
4361
renderSeries(WPainter & painter)4362 void WCartesianChart::renderSeries(WPainter& painter) const
4363 {
4364 if (isInteractive()) {
4365 painter.save();
4366 WPainterPath clipPath;
4367 clipPath.addRect(hv(chartArea_));
4368 painter.setClipPath(clipPath);
4369 painter.setClipping(true);
4370 }
4371 barTooltips_.clear();
4372 {
4373 SeriesRenderIterator iterator(*this, painter);
4374 iterateSeries(&iterator, &painter, true);
4375 }
4376
4377 {
4378 LabelRenderIterator iterator(*this, painter);
4379 iterateSeries(&iterator, &painter);
4380 }
4381
4382 {
4383 MarkerRenderIterator iterator(*this, painter);
4384 iterateSeries(&iterator, &painter);
4385 }
4386 if (isInteractive()) {
4387 painter.restore();
4388 }
4389 }
4390
calcNumBarGroups()4391 int WCartesianChart::calcNumBarGroups()
4392 {
4393 int numBarGroups = 0;
4394
4395 bool newGroup = true;
4396 for (unsigned i = 0; i < series_.size(); ++i)
4397 if (series_[i]->type() == SeriesType::Bar) {
4398 if (newGroup || !series_[i]->isStacked())
4399 ++numBarGroups;
4400 newGroup = false;
4401 } else
4402 newGroup = true;
4403
4404 return numBarGroups;
4405 }
4406
4407
renderLegend(WPainter & painter)4408 void WCartesianChart::renderLegend(WPainter& painter) const
4409 {
4410 bool vertical = orientation() == Orientation::Vertical;
4411
4412 int w = vertical ? width_ : height_;
4413 int h = vertical ? height_ : width_;
4414
4415 // Calculate margin based on layout
4416 const int legendPadding = 10;
4417 int legendWidth = 0;
4418 int legendHeight = 0;
4419 if (isLegendEnabled()) {
4420 painter.save();
4421
4422 int numSeriesWithLegend = 0;
4423
4424 for (unsigned i = 0; i < series_.size(); ++i)
4425 if (series_[i]->isLegendEnabled())
4426 ++numSeriesWithLegend;
4427
4428 painter.setFont(legendFont());
4429 WFont f = painter.font();
4430
4431 if (isAutoLayoutEnabled() &&
4432 painter.device()->features().test(
4433 PaintDeviceFeatureFlag::FontMetrics)) {
4434 int columnWidth = 0;
4435 for (unsigned i = 0; i < series_.size(); ++i)
4436 if (series_[i]->isLegendEnabled()) {
4437 WString s = series_[i]->model()->headerData(series_[i]->modelColumn());
4438 WTextItem t = painter.device()->measureText(s);
4439 columnWidth = std::max(columnWidth, (int)t.width());
4440 }
4441
4442 columnWidth += 25;
4443 WCartesianChart *self = const_cast<WCartesianChart *>(this);
4444 self->legend_.setLegendColumnWidth(columnWidth);
4445
4446 if (legendSide() == Side::Top || legendSide() == Side::Bottom) {
4447 self->legend_.setLegendColumns(std::max(1, w / columnWidth - 1));
4448 }
4449 }
4450
4451 int numLegendRows = (numSeriesWithLegend - 1) / legendColumns() + 1;
4452 double lineHeight = f.sizeLength().toPixels() * 1.5;
4453
4454 legendWidth = (int)legendColumnWidth().toPixels()
4455 * std::min(legendColumns(), numSeriesWithLegend);
4456 legendHeight = (int) (numLegendRows * lineHeight);
4457
4458 int x = 0;
4459 int y = 0;
4460
4461 switch (legendSide()) {
4462 case Side::Left:
4463 if (legendLocation() == LegendLocation::Inside)
4464 x = plotAreaPadding(Side::Left) + legendPadding;
4465 else
4466 x = plotAreaPadding(Side::Left) - legendPadding - legendWidth;
4467 break;
4468 case Side::Right:
4469 x = w - plotAreaPadding(Side::Right);
4470 if (legendLocation() == LegendLocation::Inside)
4471 x -= legendPadding + legendWidth;
4472 else
4473 x += legendPadding;
4474 break;
4475 case Side::Top:
4476 if (legendLocation() == LegendLocation::Inside)
4477 y = plotAreaPadding(Side::Top) + legendPadding;
4478 else
4479 y = plotAreaPadding(Side::Top) - legendPadding - legendHeight;
4480 break;
4481 case Side::Bottom:
4482 y = h - plotAreaPadding(Side::Bottom);
4483 if (legendLocation() == LegendLocation::Inside)
4484 y -= legendPadding + legendHeight;
4485 else
4486 y += legendPadding;
4487 default:
4488 break;
4489 }
4490
4491 switch (legendAlignment()) {
4492 case AlignmentFlag::Top:
4493 y = plotAreaPadding(Side::Top) + legendPadding;
4494 break;
4495 case AlignmentFlag::Middle:
4496 {
4497 double middle = plotAreaPadding(Side::Top)
4498 + (h - plotAreaPadding(Side::Top) - plotAreaPadding(Side::Bottom)) / 2;
4499
4500 y = (int) (middle - legendHeight/2);
4501 }
4502 break;
4503 case AlignmentFlag::Bottom:
4504 y = h - plotAreaPadding(Side::Bottom) - legendPadding - legendHeight;
4505 break;
4506 case AlignmentFlag::Left:
4507 x = plotAreaPadding(Side::Left) + legendPadding;
4508 break;
4509 case AlignmentFlag::Center:
4510 {
4511 double center = plotAreaPadding(Side::Left)
4512 + (w - plotAreaPadding(Side::Left) - plotAreaPadding(Side::Right)) / 2;
4513
4514 x = (int) (center - legendWidth/2);
4515 }
4516 break;
4517 case AlignmentFlag::Right:
4518 x = w - plotAreaPadding(Side::Right) - legendPadding - legendWidth;
4519 break;
4520 default:
4521 break;
4522 }
4523
4524 int xOffset = 0;
4525 int yOffset = 0;
4526 if (legendLocation() == LegendLocation::Outside) {
4527 switch (legendSide()) {
4528 case Side::Top: {
4529 if (orientation() == Orientation::Horizontal) {
4530 for (int i = yAxisCount() - 1; i >= 0; --i) {
4531 if (yAxis(i).isVisible() &&
4532 (yAxes_[i].location.initLoc == AxisValue::Minimum ||
4533 yAxes_[i].location.initLoc == AxisValue::Both)) {
4534 yOffset = - (yAxes_[i].location.minOffset +
4535 yAxes_[i].calculatedWidth);
4536 break;
4537 }
4538 }
4539 } else {
4540 for (int i = xAxisCount() - 1; i >= 0; --i) {
4541 if (xAxis(i).isVisible() &&
4542 (xAxes_[i].location.initLoc == AxisValue::Maximum ||
4543 xAxes_[i].location.initLoc == AxisValue::Both)) {
4544 yOffset = - (xAxes_[i].location.minOffset +
4545 xAxes_[i].calculatedWidth);
4546 break;
4547 }
4548 }
4549 }
4550 yOffset -= 5;
4551 }
4552 break;
4553 case Side::Bottom: {
4554 if (orientation() == Orientation::Horizontal) {
4555 for (int i = yAxisCount() - 1; i >= 0; --i) {
4556 if (yAxis(i).isVisible() &&
4557 (yAxes_[i].location.initLoc == AxisValue::Maximum ||
4558 yAxes_[i].location.initLoc == AxisValue::Both)) {
4559 yOffset = yAxes_[i].location.maxOffset +
4560 yAxes_[i].calculatedWidth;
4561 break;
4562 }
4563 }
4564 } else {
4565 for (int i = xAxisCount() - 1; i >= 0; --i) {
4566 if (xAxis(i).isVisible() &&
4567 (xAxes_[i].location.initLoc == AxisValue::Minimum ||
4568 xAxes_[i].location.initLoc == AxisValue::Both)) {
4569 yOffset = xAxes_[i].location.maxOffset +
4570 xAxes_[i].calculatedWidth;
4571 break;
4572 }
4573 }
4574 }
4575 yOffset += 5;
4576 }
4577 break;
4578 case Side::Left: {
4579 if (orientation() == Orientation::Horizontal) {
4580 for (int i = xAxisCount() - 1; i >= 0; --i) {
4581 if (xAxis(i).isVisible() &&
4582 (xAxes_[i].location.initLoc == AxisValue::Minimum ||
4583 xAxes_[i].location.initLoc == AxisValue::Both)) {
4584 xOffset = - (xAxes_[i].location.minOffset +
4585 xAxes_[i].calculatedWidth);
4586 break;
4587 }
4588 }
4589 } else {
4590 for (int i = yAxisCount() - 1; i >= 0; --i) {
4591 if (yAxis(i).isVisible() &&
4592 (yAxes_[i].location.initLoc == AxisValue::Minimum ||
4593 yAxes_[i].location.initLoc == AxisValue::Both)) {
4594 xOffset = - (yAxes_[i].location.minOffset +
4595 yAxes_[i].calculatedWidth);
4596 break;
4597 }
4598 }
4599 }
4600 xOffset -= 5;
4601 }
4602 break;
4603 case Side::Right: {
4604 if (orientation() == Orientation::Horizontal) {
4605 for (int i = xAxisCount() - 1; i >= 0; --i) {
4606 if (xAxis(i).isVisible() &&
4607 (xAxes_[i].location.initLoc == AxisValue::Maximum ||
4608 xAxes_[i].location.initLoc == AxisValue::Both)) {
4609 xOffset = xAxes_[i].location.maxOffset +
4610 xAxes_[i].calculatedWidth;
4611 break;
4612 }
4613 }
4614 } else {
4615 for (int i = yAxisCount() - 1; i >= 0; --i) {
4616 if (yAxis(i).isVisible() &&
4617 (yAxes_[i].location.initLoc == AxisValue::Maximum ||
4618 yAxes_[i].location.initLoc == AxisValue::Both)) {
4619 xOffset = yAxes_[i].location.maxOffset +
4620 yAxes_[i].calculatedWidth;
4621 break;
4622 }
4623 }
4624 }
4625 xOffset += 5;
4626 }
4627 break;
4628 }
4629 } else {
4630 switch (legendSide()) {
4631 case Side::Top:
4632 yOffset = 5;
4633 break;
4634 case Side::Bottom:
4635 yOffset = -5;
4636 break;
4637 case Side::Left:
4638 xOffset = 5;
4639 break;
4640 case Side::Right:
4641 xOffset = -5;
4642 break;
4643 }
4644 }
4645
4646 #ifdef WT_TARGET_JAVA
4647 painter.setPen(WPen(legendBorder()));
4648 #else
4649 painter.setPen(legendBorder());
4650 #endif
4651 painter.setBrush(legendBackground());
4652
4653 painter.drawRect(x + xOffset - legendPadding/2, y + yOffset - legendPadding/2, legendWidth + legendPadding, legendHeight + legendPadding);
4654
4655 painter.setPen(WPen());
4656
4657 painter.setFont(legendFont());
4658
4659 int item = 0;
4660 for (unsigned i = 0; i < series_.size(); ++i)
4661 if (series_[i]->isLegendEnabled()) {
4662 int col = item % legendColumns();
4663 int row = item / legendColumns();
4664 double itemX = x + xOffset + col * legendColumnWidth().toPixels();
4665 double itemY = y + yOffset + row * lineHeight;
4666
4667 renderLegendItem(painter, WPointF(itemX, itemY + lineHeight/2),
4668 *series_[i]);
4669
4670 ++item;
4671 }
4672
4673 painter.restore();
4674 }
4675
4676 if (!title().empty()) {
4677 int x = plotAreaPadding(Side::Left)
4678 + (w - plotAreaPadding(Side::Left) - plotAreaPadding(Side::Right)) / 2 ;
4679 painter.save();
4680 painter.setFont(titleFont());
4681 double titleHeight = titleFont().sizeLength().toPixels();
4682 const int TITLE_PADDING = 10;
4683 const int TITLE_WIDTH = 1000;
4684 // Extra space required for axes above the chart + legend
4685 int titleOffset = 0;
4686 if (orientation() == Orientation::Horizontal) {
4687 for (int i = yAxisCount() - 1; i >= 0; --i) {
4688 if (yAxes_[i].location.initLoc == AxisValue::Minimum ||
4689 yAxes_[i].location.initLoc == AxisValue::Both) {
4690 titleOffset = yAxes_[i].location.minOffset +
4691 yAxes_[i].calculatedWidth;
4692 break;
4693 }
4694 }
4695 } else {
4696 for (int i = xAxisCount() - 1; i >= 0; --i) {
4697 if (xAxes_[i].location.initLoc == AxisValue::Maximum ||
4698 xAxes_[i].location.initLoc == AxisValue::Both) {
4699 titleOffset = xAxes_[i].location.minOffset +
4700 xAxes_[i].calculatedWidth;
4701 break;
4702 }
4703 }
4704 }
4705 if (legendSide() == Side::Top &&
4706 legendLocation() == LegendLocation::Outside) {
4707 titleOffset += legendHeight + legendPadding + 5;
4708 }
4709 painter.drawText(x - TITLE_WIDTH / 2,
4710 plotAreaPadding(Side::Top) - titleHeight - TITLE_PADDING - titleOffset,
4711 TITLE_WIDTH, titleHeight, WFlags<AlignmentFlag>(AlignmentFlag::Center) | AlignmentFlag::Top, title());
4712 painter.restore();
4713 }
4714 }
4715
renderOther(WPainter & painter)4716 void WCartesianChart::renderOther(WPainter &painter) const
4717 {
4718 WPainterPath clipPath;
4719 clipPath.addRect(hv(chartArea_));
4720 painter.setClipPath(clipPath);
4721 }
4722
renderLabel(WPainter & painter,const WString & text,const WPointF & p,WFlags<AlignmentFlag> flags,double angle,int margin)4723 void WCartesianChart::renderLabel(WPainter& painter, const WString& text,
4724 const WPointF& p,
4725 WFlags<AlignmentFlag> flags,
4726 double angle, int margin) const
4727 {
4728 AlignmentFlag horizontalAlign = flags & AlignHorizontalMask;
4729 AlignmentFlag verticalAlign = flags & AlignVerticalMask;
4730
4731 AlignmentFlag rHorizontalAlign = horizontalAlign;
4732 AlignmentFlag rVerticalAlign = verticalAlign;
4733
4734 double width = 1000;
4735 double height = 20;
4736
4737 WPointF pos = hv(p);
4738
4739 if (orientation() == Orientation::Horizontal) {
4740 switch (horizontalAlign) {
4741 case AlignmentFlag::Left:
4742 rVerticalAlign = AlignmentFlag::Top; break;
4743 case AlignmentFlag::Center:
4744 rVerticalAlign = AlignmentFlag::Middle; break;
4745 case AlignmentFlag::Right:
4746 rVerticalAlign = AlignmentFlag::Bottom; break;
4747 default:
4748 break;
4749 }
4750
4751 switch (verticalAlign) {
4752 case AlignmentFlag::Top:
4753 rHorizontalAlign = AlignmentFlag::Right; break;
4754 case AlignmentFlag::Middle:
4755 rHorizontalAlign = AlignmentFlag::Center; break;
4756 case AlignmentFlag::Bottom:
4757 rHorizontalAlign = AlignmentFlag::Left; break;
4758 default:
4759 break;
4760 }
4761 }
4762
4763 double left = 0;
4764 double top = 0;
4765
4766 switch (rHorizontalAlign) {
4767 case AlignmentFlag::Left:
4768 left += margin; break;
4769 case AlignmentFlag::Center:
4770 left -= width/2; break;
4771 case AlignmentFlag::Right:
4772 left -= width + margin;
4773 default:
4774 break;
4775 }
4776
4777 switch (rVerticalAlign) {
4778 case AlignmentFlag::Top:
4779 top += margin; break;
4780 case AlignmentFlag::Middle:
4781 top -= height/2; break;
4782 case AlignmentFlag::Bottom:
4783 top -= height + margin; break;
4784 default:
4785 break;
4786 }
4787
4788 WPen oldPen = painter.pen();
4789 #ifdef WT_TARGET_JAVA
4790 painter.setPen(WPen(textPen_));
4791 #else
4792 painter.setPen(textPen_);
4793 #endif
4794 WTransform oldTransform = WTransform(painter.worldTransform());
4795 painter.translate(pos);
4796
4797 if (angle == 0) {
4798 painter.drawText(WRectF(left, top, width, height),
4799 WFlags<AlignmentFlag>(rHorizontalAlign) | rVerticalAlign, text);
4800 } else {
4801 painter.rotate(-angle);
4802 painter.drawText(WRectF(left, top, width, height),
4803 WFlags<AlignmentFlag>(rHorizontalAlign) | rVerticalAlign, text);
4804 }
4805
4806 painter.setWorldTransform(oldTransform, false);
4807 painter.setPen(oldPen);
4808 }
4809
hv(const WPointF & p)4810 WPointF WCartesianChart::hv(const WPointF& p) const
4811 {
4812 if (p.isJavaScriptBound()) {
4813 if (orientation() == Orientation::Vertical) {
4814 return p;
4815 } else {
4816 return p.swapHV(height_);
4817 }
4818 }
4819 return hv(p.x(), p.y());
4820 }
4821
inverseHv(const WPointF & p)4822 WPointF WCartesianChart::inverseHv(const WPointF &p) const
4823 {
4824 if (p.isJavaScriptBound()) {
4825 if (orientation() == Orientation::Vertical) {
4826 return p;
4827 } else {
4828 return p.inverseSwapHV(height_);
4829 }
4830 }
4831 return inverseHv(p.x(), p.y(), height_);
4832 }
4833
hv(double x,double y)4834 WPointF WCartesianChart::hv(double x, double y) const
4835 {
4836 return hv(x, y, height_);
4837 }
4838
hv(const WRectF & r)4839 WRectF WCartesianChart::hv(const WRectF& r) const
4840 {
4841 if (orientation() == Orientation::Vertical)
4842 return r;
4843 else {
4844 WPointF tl = hv(r.bottomLeft());
4845 return WRectF(tl.x(), tl.y(), r.height(), r.width());
4846 }
4847 }
4848
updateJSConfig(const std::string & key,cpp17::any value)4849 void WCartesianChart::updateJSConfig(const std::string &key, cpp17::any value)
4850 {
4851 if (getMethod() == RenderMethod::HtmlCanvas) {
4852 if (!cObjCreated_) {
4853 update();
4854 } else {
4855 doJavaScript(cObjJsRef() + ".updateConfig({" + key + ":" +
4856 asString(value).toUTF8() + "});");
4857 }
4858 }
4859 }
4860
setZoomEnabled(bool zoomEnabled)4861 void WCartesianChart::setZoomEnabled(bool zoomEnabled)
4862 {
4863 if (zoomEnabled_ != zoomEnabled) {
4864 zoomEnabled_ = zoomEnabled;
4865 updateJSConfig("zoom", zoomEnabled_);
4866 }
4867 }
4868
zoomEnabled()4869 bool WCartesianChart::zoomEnabled() const
4870 {
4871 return zoomEnabled_;
4872 }
4873
setPanEnabled(bool panEnabled)4874 void WCartesianChart::setPanEnabled(bool panEnabled)
4875 {
4876 if (panEnabled_ != panEnabled) {
4877 panEnabled_ = panEnabled;
4878 updateJSConfig("pan", panEnabled_);
4879 }
4880 }
4881
panEnabled()4882 bool WCartesianChart::panEnabled() const
4883 {
4884 return panEnabled_;
4885 }
4886
setCrosshairEnabled(bool crosshair)4887 void WCartesianChart::setCrosshairEnabled(bool crosshair)
4888 {
4889 if (crosshairEnabled_ != crosshair) {
4890 crosshairEnabled_ = crosshair;
4891 updateJSConfig("crosshair", crosshairEnabled_);
4892 }
4893 }
4894
crosshairEnabled()4895 bool WCartesianChart::crosshairEnabled() const
4896 {
4897 return crosshairEnabled_;
4898 }
4899
setCrosshairColor(const WColor & color)4900 void WCartesianChart::setCrosshairColor(const WColor &color)
4901 {
4902 if (crosshairColor_ != color) {
4903 crosshairColor_ = color;
4904 updateJSConfig("crosshairColor", jsStringLiteral(color.cssText(true)));
4905 }
4906 }
4907
setCrosshairXAxis(int xAxis)4908 void WCartesianChart::setCrosshairXAxis(int xAxis)
4909 {
4910 if (crosshairXAxis_ != xAxis) {
4911 crosshairXAxis_ = xAxis;
4912 updateJSConfig("crosshairXAxis", xAxis);
4913 }
4914 }
4915
setCrosshairYAxis(int yAxis)4916 void WCartesianChart::setCrosshairYAxis(int yAxis)
4917 {
4918 if (crosshairYAxis_ != yAxis) {
4919 crosshairYAxis_ = yAxis;
4920 updateJSConfig("crosshairYAxis", yAxis);
4921 }
4922 }
4923
setFollowCurve(int followCurve)4924 void WCartesianChart::setFollowCurve(int followCurve)
4925 {
4926 if (followCurve == -1) {
4927 setFollowCurve((const WDataSeries *) 0);
4928 } else {
4929 for (std::size_t i = 0; i < series_.size(); ++i) {
4930 if (series_[i]->modelColumn() == followCurve)
4931 setFollowCurve(series_[i].get());
4932 }
4933 }
4934 }
4935
setFollowCurve(const WDataSeries * series)4936 void WCartesianChart::setFollowCurve(const WDataSeries *series)
4937 {
4938 if (followCurve_ != series) {
4939 followCurve_ = series;
4940 updateJSConfig("followCurve", series ? seriesIndexOf(*series) : -1);
4941 }
4942 }
4943
disableFollowCurve()4944 void WCartesianChart::disableFollowCurve()
4945 {
4946 setFollowCurve((const WDataSeries *) 0);
4947 }
4948
followCurve()4949 const WDataSeries *WCartesianChart::followCurve() const
4950 {
4951 return followCurve_;
4952 }
4953
setRubberBandEffectEnabled(bool rubberBandEnabled)4954 void WCartesianChart::setRubberBandEffectEnabled(bool rubberBandEnabled)
4955 {
4956 if (rubberBandEnabled_ != rubberBandEnabled) {
4957 rubberBandEnabled_ = rubberBandEnabled;
4958 updateJSConfig("rubberBand", rubberBandEnabled_);
4959 }
4960 }
4961
rubberBandEffectEnabled()4962 bool WCartesianChart::rubberBandEffectEnabled() const
4963 {
4964 return rubberBandEnabled_;
4965 }
4966
wheelActionsToJson(WheelActions wheelActions)4967 std::string WCartesianChart::wheelActionsToJson(WheelActions wheelActions) {
4968 WStringStream ss;
4969 ss << '{';
4970 bool first = true;
4971 for (WheelActions::iterator it = wheelActions.begin(); it != wheelActions.end(); ++it) {
4972 if (first) first = false;
4973 else ss << ',';
4974 ss << it->first.value() << ':' << (int)it->second;
4975 }
4976 ss << '}';
4977 return ss.str();
4978 }
4979
setWheelActions(WheelActions wheelActions)4980 void WCartesianChart::setWheelActions(WheelActions wheelActions)
4981 {
4982 wheelActions_ = wheelActions;
4983
4984 updateJSConfig("wheelActions", wheelActionsToJson(wheelActions_));
4985 }
4986
clearPens()4987 void WCartesianChart::clearPens()
4988 {
4989 for (int i = 0; i < xAxisCount(); ++i) {
4990 clearPensForAxis(Axis::X, i);
4991 }
4992 for (int i = 0; i < yAxisCount(); ++i) {
4993 clearPensForAxis(Axis::Y, i);
4994 }
4995 }
4996
clearPensForAxis(Axis ax,int axisId)4997 void WCartesianChart::clearPensForAxis(Axis ax, int axisId)
4998 {
4999 std::vector<PenAssignment> &assignments =
5000 ax == Axis::X ? xAxes_[axisId].pens : yAxes_[axisId].pens;
5001 for (std::size_t i = 0; i < assignments.size(); ++i) {
5002 PenAssignment &assignment = assignments[i];
5003 freePens_.push_back(assignment.pen);
5004 freePens_.push_back(assignment.textPen);
5005 freePens_.push_back(assignment.gridPen);
5006 }
5007 assignments.clear();
5008 }
5009
createPensForAxis(Axis ax,int axisId)5010 void WCartesianChart::createPensForAxis(Axis ax, int axisId)
5011 {
5012 WAxis &axis = ax == Axis::X ? this->xAxis(axisId) : this->yAxis(axisId);
5013 if (!axis.isVisible() || axis.scale() == AxisScale::Log)
5014 return;
5015
5016 AxisStruct &axisStruct = ax == Axis::X ? xAxes_[axisId] : yAxes_[axisId];
5017
5018 double zoom = axis.zoom();
5019 if (zoom > axis.maxZoom()) {
5020 zoom = axis.maxZoom();
5021 }
5022 int level = toZoomLevel(zoom);
5023
5024 std::vector<PenAssignment> assignments;
5025 bool stop = false;
5026 for (int i = 1; !stop; ++i) {
5027 if (onDemandLoadingEnabled() &&
5028 i > level + 1)
5029 break;
5030 double z = std::pow(2.0, i-1);
5031 stop = z >= axis.maxZoom();
5032 WJavaScriptHandle<WPen> pen;
5033 if (freePens_.size() > 0) {
5034 pen = freePens_.back();
5035 freePens_.pop_back();
5036 } else {
5037 pen = createJSPen();
5038 }
5039 WPen p = WPen(axis.pen());
5040 p.setColor(WColor(p.color().red(), p.color().green(), p.color().blue(),
5041 (i == level ? p.color().alpha() : 0)));
5042 pen.setValue(p);
5043 WJavaScriptHandle<WPen> textPen;
5044 if (freePens_.size() > 0) {
5045 textPen = freePens_.back();
5046 freePens_.pop_back();
5047 } else {
5048 textPen = createJSPen();
5049 }
5050 p = WPen(axis.textPen());
5051 p.setColor(WColor(p.color().red(), p.color().green(), p.color().blue(),
5052 (i == level ? p.color().alpha() : 0)));
5053 textPen.setValue(p);
5054 WJavaScriptHandle<WPen> gridPen;
5055 if (freePens_.size() > 0) {
5056 gridPen = freePens_.back();
5057 freePens_.pop_back();
5058 } else {
5059 gridPen = createJSPen();
5060 }
5061 p = WPen(axis.gridLinesPen());
5062 p.setColor(WColor(p.color().red(), p.color().green(), p.color().blue(),
5063 (i == level ? p.color().alpha() : 0)));
5064 gridPen.setValue(p);
5065 assignments.push_back(PenAssignment(pen, textPen, gridPen));
5066 }
5067 axisStruct.pens = assignments;
5068 }
5069
zoomRangeTransform(int yAxis)5070 WTransform WCartesianChart::zoomRangeTransform(int yAxis) const
5071 {
5072 return zoomRangeTransform(xAxis(0), this->yAxis(yAxis));
5073 }
5074
zoomRangeTransform(const WAxis & xAxis,const WAxis & yAxis)5075 WTransform WCartesianChart::zoomRangeTransform(const WAxis &xAxis, const WAxis &yAxis) const
5076 {
5077 return zoomRangeTransform(xAxes_[xAxis.xAxis_].transform, yAxes_[yAxis.yAxis_].transform);
5078 }
5079
zoomRangeTransform(const WTransform & xTransform,const WTransform & yTransform)5080 WTransform WCartesianChart::zoomRangeTransform(const WTransform &xTransform, const WTransform &yTransform) const
5081 {
5082 if (orientation() == Orientation::Vertical) {
5083 return WTransform(1,0,0,-1,chartArea_.left(),chartArea_.bottom()) *
5084 xTransform * yTransform *
5085 WTransform(1,0,0,-1,-chartArea_.left(),chartArea_.bottom());
5086 } else {
5087 WRectF area = hv(chartArea_);
5088 return WTransform(0,1,1,0,area.left(),area.top()) *
5089 xTransform * yTransform *
5090 WTransform(0,1,1,0,-area.top(),-area.left());
5091 }
5092 }
5093
calculateCurveTransform(const WDataSeries & series)5094 WTransform WCartesianChart::calculateCurveTransform(const WDataSeries &series) const
5095 {
5096 const WAxis &xAxis = this->xAxis(series.xAxis());
5097 const WAxis &yAxis = this->yAxis(series.yAxis());
5098 double origin;
5099 if (orientation() == Orientation::Horizontal) {
5100 origin = mapToDeviceWithoutTransform(0.0, 0.0, xAxis, yAxis).x();
5101 } else {
5102 origin = mapToDeviceWithoutTransform(0.0, 0.0, xAxis, yAxis).y();
5103 }
5104 double offset = yAxis.mapToDevice(0.0, 0) - yAxis.mapToDevice(series.offset(), 0);
5105 if (orientation() == Orientation::Horizontal) offset = -offset;
5106 return WTransform(1, 0, 0, series.scale(), 0, origin * (1 - series.scale()) + offset);
5107 }
5108
curveTransform(const WDataSeries & series)5109 WTransform WCartesianChart::curveTransform(const WDataSeries &series) const
5110 {
5111 TransformMap::const_iterator it = curveTransforms_.find(&series);
5112 WTransform t;
5113 if (it == curveTransforms_.end()) {
5114 t = calculateCurveTransform(series);
5115 } else {
5116 t = it->second.value();
5117 }
5118 if (orientation() == Orientation::Vertical) {
5119 return t;
5120 } else {
5121 return WTransform(0,1,1,0,0,0) * t * WTransform(0,1,1,0,0,0);
5122 }
5123 }
5124
cObjJsRef()5125 std::string WCartesianChart::cObjJsRef() const
5126 {
5127 return jsRef() + ".wtCObj";
5128 }
5129
addAxisSliderWidget(WAxisSliderWidget * slider)5130 void WCartesianChart::addAxisSliderWidget(WAxisSliderWidget *slider)
5131 {
5132 axisSliderWidgets_.push_back(slider);
5133 WStringStream ss;
5134 ss << '[';
5135 for (std::size_t i = 0; i < axisSliderWidgets_.size(); ++i) {
5136 if (i != 0) ss << ',';
5137 ss << '"' << axisSliderWidgets_[i]->id() << '"';
5138 }
5139 ss << ']';
5140 updateJSConfig("sliders", ss.str());
5141 }
5142
removeAxisSliderWidget(WAxisSliderWidget * slider)5143 void WCartesianChart::removeAxisSliderWidget(WAxisSliderWidget *slider)
5144 {
5145 for (std::size_t i = 0; i < axisSliderWidgets_.size(); ++i) {
5146 if (slider == axisSliderWidgets_[i]) {
5147 axisSliderWidgets_.erase(axisSliderWidgets_.begin() + i);
5148 WStringStream ss;
5149 ss << '[';
5150 for (std::size_t j = 0; j < axisSliderWidgets_.size(); ++j) {
5151 if (j != 0) ss << ',';
5152 ss << '"' << axisSliderWidgets_[j]->id() << '"';
5153 }
5154 ss << ']';
5155 updateJSConfig("sliders", ss.str());
5156 return;
5157 }
5158 }
5159 }
5160
addAreaMask()5161 void WCartesianChart::addAreaMask()
5162 {
5163 WRectF all = hv(WRectF(0, 0, width_, height_));
5164 WRectF chart = hv(chartArea_);
5165 std::vector<WRectF> rects;
5166
5167 rects.push_back(WRectF(all.topLeft(),
5168 WPointF(all.right(), chart.top())));
5169 rects.push_back(WRectF(WPointF(all.left(), chart.bottom()),
5170 all.bottomRight()));
5171 rects.push_back(WRectF(WPointF(all.left(), chart.top()),
5172 chart.bottomLeft()));
5173 rects.push_back(WRectF(chart.topRight(),
5174 WPointF(all.right(), chart.bottom())));
5175
5176 for (std::size_t i = 0; i < rects.size(); ++i) {
5177 if (rects[i].height() > 0 && rects[i].width() > 0) {
5178 std::unique_ptr<WRectArea> rect(new WRectArea(rects[i]));
5179 rect->setHole(true);
5180 rect->setTransformable(false);
5181 addArea(std::move(rect));
5182 }
5183 }
5184 }
5185
xTransformChanged(int xAxis)5186 void WCartesianChart::xTransformChanged(int xAxis)
5187 {
5188 if (onDemandLoadingEnabled()) {
5189 update();
5190 }
5191
5192 // setFormData() already assigns the right values
5193 this->xAxis(xAxis).zoomRangeChanged().emit(this->xAxis(xAxis).zoomMinimum(),
5194 this->xAxis(xAxis).zoomMaximum());
5195 }
5196
yTransformChanged(int yAxis)5197 void WCartesianChart::yTransformChanged(int yAxis)
5198 {
5199 if (onDemandLoadingEnabled()) {
5200 update();
5201 }
5202
5203 // setFormData() already assigns the right values
5204 this->yAxis(yAxis).zoomRangeChanged().emit(this->yAxis(yAxis).zoomMinimum(),
5205 this->yAxis(yAxis).zoomMaximum());
5206 }
5207
jsSeriesSelected(double x,double y)5208 void WCartesianChart::jsSeriesSelected(double x, double y)
5209 {
5210 if (!seriesSelectionEnabled())
5211 return;
5212 double smallestSqDistance = std::numeric_limits<double>::infinity();
5213 const WDataSeries *closestSeries = nullptr;
5214 WPointF closestPointPx;
5215 WPointF closestPointBeforeSeriesTransform;
5216 for (std::size_t i = 0; i < series_.size(); ++i) {
5217 const WDataSeries &series = *series_[i];
5218 if (!series.isHidden() && (series.type() == SeriesType::Line || series.type() == SeriesType::Curve)) {
5219 WTransform transform = zoomRangeTransform(xAxes_[series.xAxis()].transformHandle.value(),
5220 yAxes_[series.yAxis()].transformHandle.value());
5221 WPointF p = transform.inverted().map(WPointF(x,y));
5222 WPainterPath path = pathForSeries(series);
5223 WTransform t = curveTransform(series);
5224 for (std::size_t j = 0; j < path.segments().size(); ++j) {
5225 const WPainterPath::Segment &seg = path.segments()[j];
5226 if (seg.type() != CubicC1 &&
5227 seg.type() != CubicC2 &&
5228 seg.type() != QuadC) {
5229 WPointF segP = t.map(WPointF(seg.x(), seg.y()));
5230 double dx = p.x() - segP.x();
5231 double dy = p.y() - segP.y();
5232 double d2 = dx * dx + dy * dy;
5233 if (d2 < smallestSqDistance) {
5234 smallestSqDistance = d2;
5235 closestSeries = &series;
5236 closestPointPx = segP;
5237 closestPointBeforeSeriesTransform = WPointF(seg.x(), seg.y());
5238 }
5239 }
5240 }
5241 }
5242 }
5243 {
5244 WTransform transform = zoomRangeTransform(xAxes_[closestSeries ? closestSeries->xAxis() : 0].transformHandle.value(),
5245 yAxes_[closestSeries ? closestSeries->yAxis() : 0].transformHandle.value());
5246 WPointF closestDisplayPoint = transform.map(closestPointPx);
5247 double dx = closestDisplayPoint.x() - x;
5248 double dy = closestDisplayPoint.y() - y;
5249 double d2 = dx * dx + dy * dy;
5250 if (d2 > CURVE_SELECTION_DISTANCE_SQUARED) {
5251 return;
5252 }
5253 }
5254 setSelectedSeries(closestSeries);
5255 if (closestSeries) {
5256 seriesSelected_.emit(closestSeries,
5257 mapFromDeviceWithoutTransform(closestPointBeforeSeriesTransform, closestSeries->axis()));
5258 } else {
5259 seriesSelected_.emit(0, mapFromDeviceWithoutTransform(closestPointBeforeSeriesTransform, Axis::Y));
5260 }
5261 }
5262
loadTooltip(double x,double y)5263 void WCartesianChart::loadTooltip(double x, double y)
5264 {
5265 std::vector<double> pxs;
5266 std::vector<double> rxs;
5267 std::vector<double> pys;
5268 std::vector<double> rys;
5269 for (int i = 0; i < xAxisCount(); ++i) {
5270 double px = zoomRangeTransform(xAxes_[i].transformHandle.value(), WTransform()).inverted().map(WPointF(x,0.0)).x();
5271 double rx = MarkerMatchIterator::MATCH_RADIUS / xAxes_[i].transformHandle.value().m11();
5272 pxs.push_back(px);
5273 rxs.push_back(rx);
5274 for (int j = 0; j < yAxisCount(); ++j) {
5275 WPointF p = zoomRangeTransform(WTransform(), yAxes_[j].transformHandle.value()).inverted().map(WPointF(0.0,y));
5276 pys.push_back(p.y());
5277 rys.push_back(MarkerMatchIterator::MATCH_RADIUS / yAxes_[j].transformHandle.value().m22());
5278 }
5279 }
5280 MarkerMatchIterator iterator(*this, pxs, pys, rxs, rys);
5281 iterateSeries(&iterator, 0);
5282
5283 if (iterator.matchedSeries()) {
5284 const WDataSeries &series = *iterator.matchedSeries();
5285 WString tooltip = series.model()->toolTip(iterator.yRow(), iterator.yColumn());
5286 bool isDeferred = series.model()->flags(iterator.yRow(), iterator.yColumn()).test(ItemFlag::DeferredToolTip);
5287 bool isXHTML = series.model()->flags(iterator.yRow(), iterator.yColumn()).test(ItemFlag::XHTMLText);
5288 if (!tooltip.empty() && (isDeferred | isXHTML)) {
5289 if (isXHTML) {
5290 bool res = removeScript(tooltip);
5291 if (!res) {
5292 tooltip = escapeText(tooltip);
5293 }
5294 } else {
5295 tooltip = escapeText(tooltip);
5296 }
5297 doJavaScript(cObjJsRef() + ".updateTooltip(" + tooltip.jsStringLiteral() + ");");
5298 }
5299 } else {
5300 for (std::size_t btt = 0; btt < barTooltips_.size(); ++btt) {
5301 const WT_ARRAY double *xs = barTooltips_[btt].xs;
5302 const WT_ARRAY double *ys = barTooltips_[btt].ys;
5303 int j = 0;
5304 int k = 3;
5305 bool c = false;
5306 WPointF p = zoomRangeTransform(
5307 xAxes_[barTooltips_[btt].series->xAxis()].transformHandle.value(),
5308 yAxes_[barTooltips_[btt].series->yAxis()].transformHandle.value())
5309 .inverted().map(WPointF(x,y));
5310 for (; j < 4; k = j++) {
5311 if ((((ys[j]<=p.y()) && (p.y()<ys[k])) ||
5312 ((ys[k]<=p.y()) && (p.y()<ys[j]))) &&
5313 (p.x() < (xs[k] - xs[j]) * (p.y() - ys[j]) / (ys[k] - ys[j]) + xs[j]))
5314 c = !c;
5315 }
5316 if (c) {
5317 WString tooltip = barTooltips_[btt].series->model()->toolTip(barTooltips_[btt].yRow, barTooltips_[btt].yColumn);
5318 if (!tooltip.empty()) {
5319 doJavaScript(cObjJsRef() + ".updateTooltip(" + escapeText(tooltip, false).jsStringLiteral() + ");");
5320 }
5321 return;
5322 }
5323 }
5324 }
5325 }
5326
5327 }
5328 }
5329