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