1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Charts module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 or (at your option) any later version
20** approved by the KDE Free Qt Foundation. The licenses are as published by
21** the Free Software Foundation and appearing in the file LICENSE.GPL3
22** included in the packaging of this file. Please review the following
23** information to ensure the GNU General Public License requirements will
24** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25**
26** $QT_END_LICENSE$
27**
28****************************************************************************/
29
30#include <private/linechartitem_p.h>
31#include <QtCharts/QLineSeries>
32#include <private/qlineseries_p.h>
33#include <private/chartpresenter_p.h>
34#include <private/polardomain_p.h>
35#include <private/chartthememanager_p.h>
36#include <private/charttheme_p.h>
37#include <QtGui/QPainter>
38#include <QtWidgets/QGraphicsSceneMouseEvent>
39
40QT_CHARTS_BEGIN_NAMESPACE
41
42LineChartItem::LineChartItem(QLineSeries *series, QGraphicsItem *item)
43 : XYChart(series,item),
44 m_series(series),
45 m_pointsVisible(false),
46 m_chartType(QChart::ChartTypeUndefined),
47 m_pointLabelsVisible(false),
48 m_pointLabelsFormat(series->pointLabelsFormat()),
49 m_pointLabelsFont(series->pointLabelsFont()),
50 m_pointLabelsColor(series->pointLabelsColor()),
51 m_pointLabelsClipping(true),
52 m_mousePressed(false)
53{
54 setAcceptHoverEvents(true);
55 setFlag(flag: QGraphicsItem::ItemIsSelectable);
56 setZValue(ChartPresenter::LineChartZValue);
57 QObject::connect(sender: series->d_func(), SIGNAL(updated()), receiver: this, SLOT(handleUpdated()));
58 QObject::connect(sender: series, SIGNAL(visibleChanged()), receiver: this, SLOT(handleUpdated()));
59 QObject::connect(sender: series, SIGNAL(opacityChanged()), receiver: this, SLOT(handleUpdated()));
60 QObject::connect(sender: series, SIGNAL(pointLabelsFormatChanged(QString)),
61 receiver: this, SLOT(handleUpdated()));
62 QObject::connect(sender: series, SIGNAL(pointLabelsVisibilityChanged(bool)),
63 receiver: this, SLOT(handleUpdated()));
64 QObject::connect(sender: series, SIGNAL(pointLabelsFontChanged(QFont)), receiver: this, SLOT(handleUpdated()));
65 QObject::connect(sender: series, SIGNAL(pointLabelsColorChanged(QColor)), receiver: this, SLOT(handleUpdated()));
66 QObject::connect(sender: series, SIGNAL(pointLabelsClippingChanged(bool)), receiver: this, SLOT(handleUpdated()));
67 handleUpdated();
68}
69
70QRectF LineChartItem::boundingRect() const
71{
72 return m_rect;
73}
74
75QPainterPath LineChartItem::shape() const
76{
77 return m_shapePath;
78}
79
80void LineChartItem::updateGeometry()
81{
82 if (m_series->useOpenGL()) {
83 if (!m_rect.isEmpty()) {
84 prepareGeometryChange();
85 // Changed signal seems to trigger even with empty region
86 m_rect = QRectF();
87 }
88 update();
89 return;
90 }
91
92 // Store the points to a local variable so that the old line gets properly cleared
93 // when animation starts.
94 m_linePoints = geometryPoints();
95 const QVector<QPointF> &points = m_linePoints;
96
97 if (points.size() == 0) {
98 prepareGeometryChange();
99 m_fullPath = QPainterPath();
100 m_linePath = QPainterPath();
101 m_rect = QRect();
102 return;
103 }
104
105 QPainterPath linePath;
106 QPainterPath fullPath;
107 // Use worst case scenario to determine required margin.
108 qreal margin = m_linePen.width() * 1.42;
109
110 // Area series use component line series that aren't necessarily added to the chart themselves,
111 // so check if chart type is forced before trying to obtain it from the chart.
112 QChart::ChartType chartType = m_chartType;
113 if (chartType == QChart::ChartTypeUndefined)
114 chartType = m_series->chart()->chartType();
115
116 // For polar charts, we need special handling for angular (horizontal)
117 // points that are off-grid.
118 if (chartType == QChart::ChartTypePolar) {
119 QPainterPath linePathLeft;
120 QPainterPath linePathRight;
121 QPainterPath *currentSegmentPath = 0;
122 QPainterPath *previousSegmentPath = 0;
123 qreal minX = domain()->minX();
124 qreal maxX = domain()->maxX();
125 qreal minY = domain()->minY();
126 QPointF currentSeriesPoint = m_series->at(index: 0);
127 QPointF currentGeometryPoint = points.at(i: 0);
128 QPointF previousGeometryPoint = points.at(i: 0);
129 int size = m_linePen.width();
130 bool pointOffGrid = false;
131 bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX);
132
133 qreal domainRadius = domain()->size().height() / 2.0;
134 const QPointF centerPoint(domainRadius, domainRadius);
135
136 if (!previousPointWasOffGrid) {
137 fullPath.moveTo(p: points.at(i: 0));
138 if (m_pointsVisible && currentSeriesPoint.y() >= minY) {
139 // Do not draw ellipses for points below minimum Y.
140 linePath.addEllipse(center: points.at(i: 0), rx: size, ry: size);
141 fullPath.addEllipse(center: points.at(i: 0), rx: size, ry: size);
142 linePath.moveTo(p: points.at(i: 0));
143 fullPath.moveTo(p: points.at(i: 0));
144 }
145 }
146
147 qreal leftMarginLine = centerPoint.x() - margin;
148 qreal rightMarginLine = centerPoint.x() + margin;
149 qreal horizontal = centerPoint.y();
150
151 // See ScatterChartItem::updateGeometry() for explanation why seriesLastIndex is needed
152 const int seriesLastIndex = m_series->count() - 1;
153
154 for (int i = 1; i < points.size(); i++) {
155 // Interpolating line fragments would be ugly when thick pen is used,
156 // so we work around it by utilizing three separate
157 // paths for line segments and clip those with custom regions at paint time.
158 // "Right" path contains segments that cross the axis line with visible point on the
159 // right side of the axis line, as well as segments that have one point within the margin
160 // on the right side of the axis line and another point on the right side of the chart.
161 // "Left" path contains points with similarly on the left side.
162 // "Full" path contains rest of the points.
163 // This doesn't yield perfect results always. E.g. when segment covers more than 90
164 // degrees and both of the points are within the margin, one in the top half and one in the
165 // bottom half of the chart, the bottom one gets clipped incorrectly.
166 // However, this should be rare occurrence in any sensible chart.
167 currentSeriesPoint = m_series->at(index: qMin(a: seriesLastIndex, b: i));
168 currentGeometryPoint = points.at(i);
169 pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX);
170
171 // Draw something unless both off-grid
172 if (!pointOffGrid || !previousPointWasOffGrid) {
173 QPointF intersectionPoint;
174 qreal y;
175 if (pointOffGrid != previousPointWasOffGrid) {
176 if (currentGeometryPoint.x() == previousGeometryPoint.x()) {
177 y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) / 2.0;
178 } else {
179 qreal ratio = (centerPoint.x() - currentGeometryPoint.x()) / (currentGeometryPoint.x() - previousGeometryPoint.x());
180 y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) * ratio;
181 }
182 intersectionPoint = QPointF(centerPoint.x(), y);
183 }
184
185 bool dummyOk; // We know points are ok, but this is needed
186 qreal currentAngle = 0;
187 qreal previousAngle = 0;
188 if (const PolarDomain *pd = qobject_cast<const PolarDomain *>(object: domain())) {
189 currentAngle = pd->toAngularCoordinate(value: currentSeriesPoint.x(), ok&: dummyOk);
190 previousAngle = pd->toAngularCoordinate(value: m_series->at(index: i - 1).x(), ok&: dummyOk);
191 } else {
192 qWarning() << Q_FUNC_INFO << "Unexpected domain: " << domain();
193 }
194 if ((qAbs(t: currentAngle - previousAngle) > 180.0)) {
195 // If the angle between two points is over 180 degrees (half X range),
196 // any direct segment between them becomes meaningless.
197 // In this case two line segments are drawn instead, from previous
198 // point to the center and from center to current point.
199 if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine))
200 && previousGeometryPoint.y() < horizontal) {
201 currentSegmentPath = &linePathRight;
202 } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine))
203 && previousGeometryPoint.y() < horizontal) {
204 currentSegmentPath = &linePathLeft;
205 } else if (previousAngle > 0.0 && previousAngle < 360.0) {
206 currentSegmentPath = &linePath;
207 } else {
208 currentSegmentPath = 0;
209 }
210
211 if (currentSegmentPath) {
212 if (previousSegmentPath != currentSegmentPath)
213 currentSegmentPath->moveTo(p: previousGeometryPoint);
214 if (previousPointWasOffGrid)
215 fullPath.moveTo(p: intersectionPoint);
216
217 currentSegmentPath->lineTo(p: centerPoint);
218 fullPath.lineTo(p: centerPoint);
219 }
220
221 previousSegmentPath = currentSegmentPath;
222
223 if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine))
224 && currentGeometryPoint.y() < horizontal) {
225 currentSegmentPath = &linePathRight;
226 } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &&currentGeometryPoint.x() > leftMarginLine))
227 && currentGeometryPoint.y() < horizontal) {
228 currentSegmentPath = &linePathLeft;
229 } else if (currentAngle > 0.0 && currentAngle < 360.0) {
230 currentSegmentPath = &linePath;
231 } else {
232 currentSegmentPath = 0;
233 }
234
235 if (currentSegmentPath) {
236 if (previousSegmentPath != currentSegmentPath)
237 currentSegmentPath->moveTo(p: centerPoint);
238 if (!previousSegmentPath)
239 fullPath.moveTo(p: centerPoint);
240
241 currentSegmentPath->lineTo(p: currentGeometryPoint);
242 if (pointOffGrid)
243 fullPath.lineTo(p: intersectionPoint);
244 else
245 fullPath.lineTo(p: currentGeometryPoint);
246 }
247 } else {
248 if (previousAngle < 0.0 || currentAngle < 0.0
249 || ((previousAngle <= 180.0 && currentAngle <= 180.0)
250 && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal)
251 || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) {
252 currentSegmentPath = &linePathRight;
253 } else if (previousAngle > 360.0 || currentAngle > 360.0
254 || ((previousAngle > 180.0 && currentAngle > 180.0)
255 && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal)
256 || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) {
257 currentSegmentPath = &linePathLeft;
258 } else {
259 currentSegmentPath = &linePath;
260 }
261
262 if (currentSegmentPath != previousSegmentPath)
263 currentSegmentPath->moveTo(p: previousGeometryPoint);
264 if (previousPointWasOffGrid)
265 fullPath.moveTo(p: intersectionPoint);
266
267 if (pointOffGrid)
268 fullPath.lineTo(p: intersectionPoint);
269 else
270 fullPath.lineTo(p: currentGeometryPoint);
271 currentSegmentPath->lineTo(p: currentGeometryPoint);
272 }
273 } else {
274 currentSegmentPath = 0;
275 }
276
277 previousPointWasOffGrid = pointOffGrid;
278 if (m_pointsVisible && !pointOffGrid && currentSeriesPoint.y() >= minY) {
279 linePath.addEllipse(center: points.at(i), rx: size, ry: size);
280 fullPath.addEllipse(center: points.at(i), rx: size, ry: size);
281 linePath.moveTo(p: points.at(i));
282 fullPath.moveTo(p: points.at(i));
283 }
284 previousSegmentPath = currentSegmentPath;
285 previousGeometryPoint = currentGeometryPoint;
286 }
287 m_linePathPolarRight = linePathRight;
288 m_linePathPolarLeft = linePathLeft;
289 // Note: This construction of m_fullpath is not perfect. The partial segments that are
290 // outside left/right clip regions at axis boundary still generate hover/click events,
291 // because shape doesn't get clipped. It doesn't seem possible to do sensibly.
292 } else { // not polar
293 linePath.moveTo(p: points.at(i: 0));
294 for (int i = 1; i < points.size(); i++)
295 linePath.lineTo(p: points.at(i));
296 fullPath = linePath;
297 }
298
299 QPainterPathStroker stroker;
300 // QPainter::drawLine does not respect join styles, for example BevelJoin becomes MiterJoin.
301 // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and
302 // multiply line width with square root of two when defining shape and bounding rectangle.
303 stroker.setWidth(margin);
304 stroker.setJoinStyle(Qt::MiterJoin);
305 stroker.setCapStyle(Qt::SquareCap);
306 stroker.setMiterLimit(m_linePen.miterLimit());
307
308 QPainterPath checkShapePath = stroker.createStroke(path: fullPath);
309
310 // Only zoom in if the bounding rects of the paths fit inside int limits. QWidget::update() uses
311 // a region that has to be compatible with QRect.
312 if (checkShapePath.boundingRect().height() <= INT_MAX
313 && checkShapePath.boundingRect().width() <= INT_MAX
314 && linePath.boundingRect().height() <= INT_MAX
315 && linePath.boundingRect().width() <= INT_MAX
316 && fullPath.boundingRect().height() <= INT_MAX
317 && fullPath.boundingRect().width() <= INT_MAX) {
318 prepareGeometryChange();
319
320 m_linePath = linePath;
321 m_fullPath = fullPath;
322 m_shapePath = checkShapePath;
323
324 m_rect = m_shapePath.boundingRect();
325 } else {
326 update();
327 }
328}
329
330void LineChartItem::handleUpdated()
331{
332 // If points visibility has changed, a geometry update is needed.
333 // Also, if pen changes when points are visible, geometry update is needed.
334 bool doGeometryUpdate =
335 (m_pointsVisible != m_series->pointsVisible())
336 || (m_series->pointsVisible() && (m_linePen != m_series->pen()));
337 bool visibleChanged = m_series->isVisible() != isVisible();
338 setVisible(m_series->isVisible());
339 setOpacity(m_series->opacity());
340 m_pointsVisible = m_series->pointsVisible();
341 m_linePen = m_series->pen();
342 m_pointLabelsFormat = m_series->pointLabelsFormat();
343 m_pointLabelsVisible = m_series->pointLabelsVisible();
344 m_pointLabelsFont = m_series->pointLabelsFont();
345 m_pointLabelsColor = m_series->pointLabelsColor();
346 bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping();
347 m_pointLabelsClipping = m_series->pointLabelsClipping();
348 if (doGeometryUpdate)
349 updateGeometry();
350 else if (m_series->useOpenGL() && visibleChanged)
351 refreshGlChart();
352
353 // Update whole chart in case label clipping changed as labels can be outside series area
354 if (labelClippingChanged)
355 m_series->chart()->update();
356 else
357 update();
358}
359
360void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
361{
362 Q_UNUSED(widget)
363 Q_UNUSED(option)
364
365 if (m_series->useOpenGL())
366 return;
367
368 QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
369 // Adjust clip rect half a pixel in required dimensions to make it include lines along the
370 // plot area edges, but never increase clip so much that any portion of the line is draw beyond
371 // the plot area.
372 const qreal x1 = pos().x() - int(pos().x());
373 const qreal y1 = pos().y() - int(pos().y());
374 const qreal x2 = (clipRect.width() + 0.5) - int(clipRect.width() + 0.5);
375 const qreal y2 = (clipRect.height() + 0.5) - int(clipRect.height() + 0.5);
376 clipRect.adjust(xp1: -x1, yp1: -y1, xp2: qMax(a: x1, b: x2), yp2: qMax(a: y1, b: y2));
377
378 painter->save();
379 painter->setPen(m_linePen);
380 bool alwaysUsePath = false;
381
382 if (m_series->chart()->chartType() == QChart::ChartTypePolar) {
383 qreal halfWidth = domain()->size().width() / 2.0;
384 QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height());
385 QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height());
386 QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse);
387 QRegion clipRegionLeft(fullPolarClipRegion.intersected(r: clipRectLeft.toRect()));
388 QRegion clipRegionRight(fullPolarClipRegion.intersected(r: clipRectRight.toRect()));
389 painter->setClipRegion(clipRegionLeft);
390 painter->drawPath(path: m_linePathPolarLeft);
391 painter->setClipRegion(clipRegionRight);
392 painter->drawPath(path: m_linePathPolarRight);
393 painter->setClipRegion(fullPolarClipRegion);
394 alwaysUsePath = true; // required for proper clipping
395 } else {
396 painter->setClipRect(clipRect);
397 }
398
399 if (m_linePen.style() != Qt::SolidLine || alwaysUsePath) {
400 // If pen style is not solid line, use path painting to ensure proper pattern continuity
401 painter->drawPath(path: m_linePath);
402 } else {
403 for (int i = 1; i < m_linePoints.size(); ++i)
404 painter->drawLine(p1: m_linePoints.at(i: i - 1), p2: m_linePoints.at(i));
405 }
406
407 if (m_pointLabelsVisible) {
408 if (m_pointLabelsClipping)
409 painter->setClipping(true);
410 else
411 painter->setClipping(false);
412 m_series->d_func()->drawSeriesPointLabels(painter, points: m_linePoints, offset: m_linePen.width() / 2);
413 }
414
415 painter->restore();
416
417 if (m_pointsVisible) {
418 // draw points that lie inside clipRect only
419 qreal ptSize = m_linePen.width() * 1.5;
420 painter->setPen(Qt::NoPen);
421 painter->setBrush(m_linePen.color());
422 for (int i = 0; i < m_linePoints.size(); ++i) {
423 if (clipRect.contains(p: m_linePoints.at(i)))
424 painter->drawEllipse(center: m_linePoints.at(i), rx: ptSize, ry: ptSize);
425 }
426 }
427}
428
429void LineChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
430{
431 emit XYChart::pressed(point: domain()->calculateDomainPoint(point: event->pos()));
432 m_lastMousePos = event->pos();
433 m_mousePressed = true;
434 QGraphicsItem::mousePressEvent(event);
435}
436
437void LineChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
438{
439 emit XYChart::hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: true);
440// event->accept();
441 QGraphicsItem::hoverEnterEvent(event);
442}
443
444void LineChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
445{
446 emit XYChart::hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: false);
447// event->accept();
448 QGraphicsItem::hoverEnterEvent(event);
449}
450
451void LineChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
452{
453 emit XYChart::released(point: domain()->calculateDomainPoint(point: m_lastMousePos));
454 if (m_mousePressed)
455 emit XYChart::clicked(point: domain()->calculateDomainPoint(point: m_lastMousePos));
456 m_mousePressed = false;
457 QGraphicsItem::mouseReleaseEvent(event);
458}
459
460void LineChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
461{
462 emit XYChart::doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos));
463 QGraphicsItem::mouseDoubleClickEvent(event);
464}
465
466QT_CHARTS_END_NAMESPACE
467
468#include "moc_linechartitem_p.cpp"
469

source code of qtcharts/src/charts/linechart/linechartitem.cpp