Revision control

++ // Copyright (C) 2024 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include <QtCore/qline.h>
++ #include <QtCore/qmath.h>
++ #include <QtGraphs/qareaseries.h>
++ #if QT_CONFIG(graphs_2d_spline)
++ #include <QtGraphs/qsplineseries.h>
++ #endif
++ #include <QtQuick/private/qquicktaphandler_p.h>
++ #include <QtQuickShapes/private/qquickshape_p.h>
++ #include <private/arearenderer_p.h>
++ #include <private/axisrenderer_p.h>
++ #include <private/pointrenderer_p.h>
++ #include <private/qabstractseries_p.h>
++ #include <private/qareaseries_p.h>
++ #include <private/qgraphsview_p.h>
++ #include <private/qpolarview_p.h>
++ #include <private/qxyseries_p.h>
++ #if QT_CONFIG(graphs_2d_high_performance_backend)
++ #include <QtCanvasPainter/QCanvasPainter>
++ #include <QtCanvasPainter/QCanvasLinearGradient>
++ #include <QtCanvasPainter/QCanvasConicalGradient>
++ #include <QtCanvasPainter/QCanvasRadialGradient>
++ #include <QLinearGradient>
++ #include <QRadialGradient>
++ #include <QConicalGradient>
++ #endif
++
++ #include <qtgraphs_tracepoints_p.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_TRACE_PREFIX(qtgraphs,
++ "QT_BEGIN_NAMESPACE" \
++ "class AreaRenderer;" \
++ "QT_END_NAMESPACE"
++ )
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRenderePointInArea_entry, int x, int y);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRenderePointInArea_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRendereAfterPolish_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRendereAfterPolish_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRendererCalculateSeriesU_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRendererCalculateSeriesU_exit);
++
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRendererCalculateSeriesL_entry);
++ Q_TRACE_POINT(qtgraphs, QGraphs2DAreaRendererCalculateSeriesL_exit);
++
++ // Rescales a radial gradient's focalRadius/centerRadius by (k, c) - see
++ // AreaRenderer::polarRadiusAffine() - so that, for a gradient centered on the polar
++ // center, sampling the unchanged original stops at the new radii reproduces exactly
++ // the color the original gradient would have shown at the corresponding fixed
++ // (pre-zoom/pan) radius. If a rescaled radius goes negative, both are shifted by the
++ // same amount instead of naively clamping just one, which preserves the
++ // (centerRadius - focalRadius) span - and thus the gradient's rate of color change.
++ static void applyPolarRadiusAffine(qreal k, qreal c, qreal *focalRadius, qreal *centerRadius)
++ {
++ qreal newFocal = c + k * (*focalRadius);
++ qreal newCenter = c + k * (*centerRadius);
++ const qreal shift = std::min(newFocal, newCenter);
++ if (shift < 0) {
++ newFocal -= shift;
++ newCenter -= shift;
++ }
++ *focalRadius = newFocal;
++ *centerRadius = newCenter;
++ }
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ static void syncGradientStops(QQuickShapeGradient *gradient, const QGradientStops &stops)
++ {
++ auto stopsProperty = gradient->stops();
++ const qsizetype existing = stopsProperty.count(&stopsProperty);
++
++ if (existing != stops.size()) {
++ // Safe to delete before clearing: the list holds raw pointers, so destroying
++ // a stop does not touch it, and no already-deleted entry is read back.
++ for (qsizetype i = 0; i < existing; ++i)
++ delete stopsProperty.at(&stopsProperty, i);
++ stopsProperty.clear(&stopsProperty);
++ for (qsizetype i = 0; i < stops.size(); ++i)
++ stopsProperty.append(&stopsProperty, new QQuickGradientStop(gradient));
++ }
++
++ for (qsizetype i = 0; i < stops.size(); ++i) {
++ auto *gradientStop = stopsProperty.at(&stopsProperty, i);
++ gradientStop->setPosition(stops.at(i).first);
++ gradientStop->setColor(stops.at(i).second);
++ }
++ }
++ #endif
++
++ AreaRenderer::AreaRenderer(QGraphsView *graph, bool clipPlotArea)
++ : QQuickItem(graph)
++ , m_graph(graph)
++ , m_polarView(qobject_cast<QPolarView *>(graph))
++ {
++ setFlag(QQuickItem::ItemHasContents);
++ setClip(clipPlotArea);
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ m_shape.setParentItem(this);
++ m_shape.setPreferredRendererType(QQuickShape::CurveRenderer);
++ #endif
++
++ m_tapHandler = new QQuickTapHandler(this);
++ connect(m_tapHandler, &QQuickTapHandler::singleTapped, this, &AreaRenderer::onSingleTapped);
++ connect(m_tapHandler, &QQuickTapHandler::doubleTapped, this, &AreaRenderer::onDoubleTapped);
++ connect(m_tapHandler, &QQuickTapHandler::pressedChanged, this, &AreaRenderer::onPressedChanged);
++ }
++
++ AreaRenderer::~AreaRenderer()
++ {
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ // m_shape is a member, so it is still alive here and its shapePaths can still be
++ // unhooked from the gradients about to be destroyed.
++ for (auto &&group : m_groups)
++ releasePolarRadialGradient(group);
++ #endif
++ qDeleteAll(m_groups);
++ }
++
++ void AreaRenderer::resetShapePathCount()
++ {
++ m_currentShapePathIndex = 0;
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ // The shape's paths are only refreshed while the canvas painter is off, so leaving it
++ // visible after a switch to the canvas painter would keep drawing the geometry from the
++ // last polish underneath that backend's output.
++ m_shape.setVisible(!m_graph->useCanvasPainter());
++ #endif
++ }
++
++ #if QT_CONFIG(graphs_2d_high_performance_backend)
++ AreaRenderer::PaintSnapshot AreaRenderer::paintSnapshot() const
++ {
++ return m_areaPaintSnapshot;
++ }
++
++ void AreaRenderer::synchronizeData()
++ {
++ m_areaPaintSnapshot.clear();
++ for (auto &&group : m_groups) {
++ if (group->painterPath.elementCount() == 0)
++ continue;
++
++ const auto style = getSeriesStyle(group);
++ AreaPaintData paintData;
++ if (auto linear = qobject_cast<QQuickShapeLinearGradient *>(style.gradient)) {
++ QLinearGradient gradient(linear->x1(), linear->y1(), linear->x2(), linear->y2());
++ gradient.setStops(linear->gradientStops());
++ paintData.gradient = gradient;
++ } else if (auto radial = qobject_cast<QQuickShapeRadialGradient *>(style.gradient)) {
++ qreal centerRadius = radial->centerRadius();
++ qreal focalRadius = radial->focalRadius();
++ qreal k, c;
++ if (polarRadiusAffine(group->series, &k, &c))
++ applyPolarRadiusAffine(k, c, &focalRadius, &centerRadius);
++ QPointF center(radial->centerX(), radial->centerY());
++ QPointF focal(radial->focalX(), radial->focalY());
++ QRadialGradient gradient(center, centerRadius, focal, focalRadius);
++ gradient.setStops(radial->gradientStops());
++ paintData.gradient = gradient;
++ } else if (auto conical = qobject_cast<QQuickShapeConicalGradient *>(style.gradient)) {
++ QConicalGradient gradient(conical->centerX(), conical->centerY(), conical->angle());
++ gradient.setStops(conical->gradientStops());
++ paintData.gradient = gradient;
++ } else {
++ paintData.resolvedColor = style.color;
++ }
++ paintData.resolvedBorderColor = style.borderColor;
++ paintData.resolvedBorderWidth = style.borderWidth;
++ paintData.painterPath = group->painterPath;
++ m_areaPaintSnapshot.append(paintData);
++ }
++ }
++
++ void AreaRenderer::paintSnapshot(const PaintSnapshot &snapshot, QCanvasPainter *p)
++ {
++ for (const auto &areaPaintSnapshot : snapshot) {
++ const auto &gradient = areaPaintSnapshot.gradient;
++ switch (gradient.type()) {
++ case QGradient::LinearGradient: {
++ const auto &lg = static_cast<const QLinearGradient &>(gradient);
++ QCanvasLinearGradient canvasGradient(lg.start().x(),
++ lg.start().y(),
++ lg.finalStop().x(),
++ lg.finalStop().y());
++ const QGradientStops stops = lg.stops();
++ for (const auto &stop : stops)
++ canvasGradient.setColorAt(stop.first, stop.second);
++ p->setFillStyle(canvasGradient);
++ } break;
++ case QGradient::RadialGradient: {
++ const auto &rg = static_cast<const QRadialGradient &>(gradient);
++ QCanvasRadialGradient canvasGradient(rg.center().x(),
++ rg.center().y(),
++ rg.focalRadius(),
++ rg.centerRadius());
++ const QGradientStops stops = rg.stops();
++ for (const auto &stop : stops)
++ canvasGradient.setColorAt(stop.first, stop.second);
++ p->setFillStyle(canvasGradient);
++ } break;
++ case QGradient::ConicalGradient: {
++ const auto &cg = static_cast<const QConicalGradient &>(gradient);
++ QCanvasConicalGradient canvasGradient(cg.center().x(), cg.center().y(), cg.angle());
++ const QGradientStops stops = cg.stops();
++ for (const auto &stop : stops)
++ canvasGradient.setColorAt(stop.first, stop.second);
++ p->setFillStyle(canvasGradient);
++ } break;
++ case QGradient::NoGradient: {
++ p->setFillStyle(areaPaintSnapshot.resolvedColor);
++ }
++ }
++ p->setStrokeStyle(areaPaintSnapshot.resolvedBorderColor);
++ p->setLineWidth(areaPaintSnapshot.resolvedBorderWidth);
++ p->beginPath();
++ p->addPath(areaPaintSnapshot.painterPath);
++ p->fill();
++ p->stroke();
++ }
++ }
++ #endif
++
++ AreaRenderer::SeriesStyle AreaRenderer::getSeriesStyle(PointGroup *group)
++ {
++ auto theme = m_graph->theme();
++
++ const auto &seriesColors = theme->seriesColors();
++ qsizetype index = group->colorIndex % seriesColors.size();
++ QColor color = group->series->color().alpha() != 0 ? group->series->color()
++ : seriesColors.at(index);
++ const auto &borderColors = theme->borderColors();
++ index = group->colorIndex % borderColors.size();
++ QColor borderColor = group->series->borderColor().alpha() != 0 ? group->series->borderColor()
++ : borderColors.at(index);
++
++ QQuickShapeGradient *gradient = group->series->gradient();
++
++ if (group->series->isSelected()) {
++ color = group->series->selectedColor().alpha() != 0 ? group->series->selectedColor()
++ : color.lighter();
++ borderColor = group->series->selectedBorderColor().alpha() != 0
++ ? group->series->selectedBorderColor()
++ : borderColor.lighter();
++ if (group->series->selectedGradient())
++ gradient = group->series->selectedGradient();
++ }
++
++ qreal borderWidth = group->series->borderWidth();
++ if (qFuzzyCompare(borderWidth, qreal(-1.0)))
++ borderWidth = theme->borderWidth();
++
++ return {color, gradient, borderColor, borderWidth};
++ }
++
++ // Radial-axis-value-to-pixel-radius mapping is affine in a polar view, so the
++ // fixed(zoomless)-radius -> live(zoomed)-radius mapping is itself affine:
++ // liveRadius = k * fixedRadius + c. Returns false if series isn't on a polar
++ // view or the axis range is degenerate.
++ bool AreaRenderer::polarRadiusAffine(QAreaSeries *series, qreal *k, qreal *c) const
++ {
++ if (!m_polarView)
++ return false;
++
++ auto &axisY = m_graph->m_axisRenderer->getAxisY(series);
++ if (qFuzzyIsNull(axisY.valueRange) || qFuzzyIsNull(axisY.valueRangeZoomless))
++ return false;
++
++ // Radius-of-value is affine (see calculateRenderCoordinates()'s polar branch), so the
++ // fixed(zoomless)-radius -> live(zoomed) radius mapping is itself affine:
++ // liveRadius = k * fixedRadius + c.
++ const qreal polarRadius = m_polarView->polarRadius();
++ *k = axisY.valueRangeZoomless / axisY.valueRange;
++ *c = polarRadius * (axisY.minValueZoomless - axisY.minValue) / axisY.valueRange;
++ return true;
++ }
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ // Returns an owned QQuickShapeRadialGradient mirroring \a radial but with
++ // centerRadius/focalRadius rescaled to the current zoom.
++ QQuickShapeRadialGradient *AreaRenderer::ensurePolarRadialGradient(PointGroup *group,
++ QQuickShapeRadialGradient *radial,
++ QAreaSeries *series)
++ {
++ qreal k, c;
++ if (!polarRadiusAffine(series, &k, &c))
++ return radial;
++
++ if (!group->cachedPolarRadialGradient)
++ group->cachedPolarRadialGradient = new QQuickShapeRadialGradient(group->shapePath);
++
++ qreal centerRadius = radial->centerRadius();
++ qreal focalRadius = radial->focalRadius();
++ applyPolarRadiusAffine(k, c, &focalRadius, &centerRadius);
++
++ auto *gradient = group->cachedPolarRadialGradient;
++ gradient->setCenterX(radial->centerX());
++ gradient->setCenterY(radial->centerY());
++ gradient->setCenterRadius(centerRadius);
++ gradient->setFocalX(radial->focalX());
++ gradient->setFocalY(radial->focalY());
++ gradient->setFocalRadius(focalRadius);
++ gradient->setSpread(radial->spread());
++ syncGradientStops(gradient, radial->gradientStops());
++
++ return gradient;
++ }
++
++ // The cached gradient is parented to whichever shapePath was current when it was
++ // created, but shapePaths outlive the group they were resolved for: they stay in the
++ // shape's data list to be reused by whichever series next takes their index. Relying
++ // on the QObject parent chain would therefore only reclaim the gradient when the whole
++ // renderer goes away, so destroy it explicitly here. Every reference has to be dropped
++ // first - QQuickShapePath holds fillGradient as a raw pointer with no destroyed()
++ // handling - and not just the one on the group's current shapePath: an index shift can
++ // leave a now-surplus shapePath still pointing at the same gradient, since afterPolish()
++ // only blanks its path.
++ void AreaRenderer::releasePolarRadialGradient(PointGroup *group)
++ {
++ if (!group->cachedPolarRadialGradient)
++ return;
++
++ auto data = m_shape.data();
++ for (qsizetype i = 0, count = data.count(&data); i < count; ++i) {
++ auto *shapePath = qobject_cast<QQuickShapePath *>(data.at(&data, i));
++ if (shapePath && shapePath->fillGradient() == group->cachedPolarRadialGradient)
++ shapePath->setFillGradient(nullptr);
++ }
++
++ delete group->cachedPolarRadialGradient;
++ group->cachedPolarRadialGradient = nullptr;
++ }
++ #endif
++
++ void AreaRenderer::calculateRenderCoordinates(
++ QAreaSeries *series, qreal origX, qreal origY, qreal *renderX, qreal *renderY) const
++ {
++ auto &axY = m_graph->m_axisRenderer->getAxisY(series);
++ auto &axX = m_graph->m_axisRenderer->getAxisX(series);
++
++ qreal x = origX;
++ qreal y = origY;
++
++ if (axX.isLogarithmic) {
++ float logBase = log(axX.logBase);
++ x = log(origX) / logBase;
++ }
++
++ if (axY.isLogarithmic) {
++ float logBase = log(axY.logBase);
++ y = log(origY) / logBase;
++ }
++
++ if (m_polarView) {
++ qreal angleFrac = !qFuzzyIsNull(axX.valueRange) ? (x - axX.minValue) / axX.valueRange : 0;
++ qreal yClamped = std::max(y, axY.minValue);
++ qreal radiusFrac = !qFuzzyIsNull(axY.valueRange) != 0 ? (yClamped - axY.minValue) / axY.valueRange : 0;
++ qreal radius = radiusFrac * m_polarView->polarRadius();
++ qreal rad = qDegreesToRadians(angleFrac * 360.0);
++ const QPointF center = m_polarView->polarCenter();
++ *renderX = center.x() + qSin(rad) * radius;
++ *renderY = center.y() - qCos(rad) * radius;
++ return;
++ }
++
++ if (m_graph->orientation() != Qt::Vertical) {
++ std::swap(x, y);
++ y = axY.maxValue - y;
++ }
++
++ *renderX = m_areaWidth * x * m_maxHorizontal - m_horizontalOffset;
++ *renderY = m_areaHeight - m_areaHeight * y * m_maxVertical
++ + m_verticalOffset;
++ }
++
++ void AreaRenderer::calculateAxisCoordinates(
++ QAreaSeries *series, qreal origX, qreal origY, qreal *axisX, qreal *axisY) const
++ {
++ auto &axY = m_graph->m_axisRenderer->getAxisY(series);
++ auto &axX = m_graph->m_axisRenderer->getAxisX(series);
++
++ if (m_graph->orientation() != Qt::Vertical) {
++ std::swap(origX, origY);
++ origY = axY.maxValue - origY;
++ }
++
++ qreal x = (origX + m_horizontalOffset) / (m_areaWidth * m_maxHorizontal);
++ qreal y = (origY - m_areaHeight - m_verticalOffset)
++ / (-1 * m_areaHeight * m_maxVertical);
++
++ if (axX.isLogarithmic)
++ x = pow(axX.logBase, x);
++
++ if (axY.isLogarithmic)
++ y = pow(axY.logBase, y);
++
++ *axisX = x;
++ *axisY = y;
++ }
++
++ void AreaRenderer::handlePolish(QAreaSeries *series)
++ {
++ auto theme = m_graph->theme();
++ if (!theme) {
++ qCCritical(lcCritical2D, "theme not found.");
++ return;
++ }
++
++ if (!m_graph->m_axisRenderer) {
++ qCCritical(lcCritical2D, "axis renderer not found.");
++ return;
++ }
++
++ QXYSeries *upper = series->upperSeries();
++ QXYSeries *lower = series->lowerSeries();
++
++ if (!upper) {
++ qCCritical(lcCritical2D, "upperSeries not found.");
++ return;
++ }
++
++ if (!m_groups.contains(series)) {
++ PointGroup *group = new PointGroup();
++ group->series = series;
++ m_groups.insert(series, group);
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ group->shapePath = new QQuickShapePath(&m_shape);
++ auto data = m_shape.data();
++ data.append(&data, m_groups.value(series)->shapePath);
++ #endif
++ }
++
++ auto group = m_groups.value(series);
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ auto data = m_shape.data();
++ group->shapePath = qobject_cast<QQuickShapePath *>(data.at(&data, m_currentShapePathIndex));
++ #endif
++
++ m_currentShapePathIndex++;
++
++ if (upper->points().count() < 2 || (lower && lower->points().count() < 2)) {
++ group->painterPath.clear();
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ group->shapePath->setPath(group->painterPath);
++ #endif
++ return;
++ }
++
++ m_areaWidth = width();
++ m_areaHeight = height();
++
++ auto &axisX = m_graph->m_axisRenderer->getAxisX(group->series);
++ auto &axisY = m_graph->m_axisRenderer->getAxisY(group->series);
++
++ m_maxVertical = axisY.valueRange > 0 ? 1.0 / axisY.valueRange : 100.0;
++ m_maxHorizontal = axisX.valueRange > 0 ? 1.0 / axisX.valueRange : 100.0;
++ m_verticalOffset = (axisY.minValue / axisY.valueRange) * m_areaHeight;
++ m_horizontalOffset = (axisX.minValue / axisX.valueRange) * m_areaWidth;
++
++ auto &painterPath = group->painterPath;
++ painterPath.clear();
++
++ if (group->colorIndex < 0) {
++ group->colorIndex = m_graph->graphSeriesCount();
++ m_graph->setGraphSeriesCount(group->colorIndex + 1);
++ }
++
++ const auto style = getSeriesStyle(group);
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ if (!m_graph->useCanvasPainter()) {
++ group->shapePath->setStrokeWidth(style.borderWidth);
++ group->shapePath->setStrokeColor(style.borderColor);
++ group->shapePath->setFillColor(style.color);
++ QQuickShapeGradient *fillGradient = style.gradient;
++ if (auto radial = qobject_cast<QQuickShapeRadialGradient *>(style.gradient))
++ fillGradient = ensurePolarRadialGradient(group, radial, series);
++ group->shapePath->setFillGradient(fillGradient);
++ group->shapePath->setCapStyle(QQuickShapePath::CapStyle::SquareCap);
++ }
++ #endif
++
++ auto &&upperPoints = upper->points();
++ QList<QPointF> fittedPoints;
++ #if QT_CONFIG(graphs_2d_spline)
++ if (upper->type() == QAbstractSeries::SeriesType::Spline)
++ fittedPoints = qobject_cast<QSplineSeries *>(upper)->getControlPoints();
++ #endif
++
++ int extraPointCount = lower ? 0 : 3;
++
++ // On a polar view, an area with no lowerSeries normally closes down to
++ // the pole (calculateRenderCoordinates() maps y == 0 to radius == 0),
++ // which is correct for a partial wedge but draws two spurious spokes
++ // through the center when the upper curve already forms a closed loop
++ // (first and last points land on the same angle). Detect that case from
++ // the actual rendered positions and just let the loop close on itself.
++ if (m_polarView && !lower && upperPoints.size() >= 2) {
++ qreal x0, y0, x1, y1;
++ calculateRenderCoordinates(series, upperPoints.first().x(), upperPoints.first().y(), &x0, &y0);
++ calculateRenderCoordinates(series, upperPoints.last().x(), upperPoints.last().y(), &x1, &y1);
++ if (QLineF(QPointF(x0, y0), QPointF(x1, y1)).length() < 0.5)
++ extraPointCount = 0;
++ }
++
++ if (series->isVisible()) {
++ Q_TRACE_SCOPE(QGraphs2DAreaRendererCalculateSeriesU);
++ for (int i = 0, j = 0; i < upperPoints.size() + extraPointCount; ++i, ++j) {
++ qreal x;
++ qreal y;
++ qreal upperX;
++ qreal upperY;
++ if (i == upperPoints.size()) {
++ upperX = upperPoints[upperPoints.size() - 1].x();
++ upperY = 0;
++ } else if (i == upperPoints.size() + 1) {
++ upperX = upperPoints[0].x();
++ upperY = 0;
++ } else if (i == upperPoints.size() + 2) {
++ upperX = upperPoints[0].x();
++ upperY = upperPoints[0].y();
++ } else {
++ upperX = upperPoints[i].x();
++ upperY = upperPoints[i].y();
++ }
++ calculateRenderCoordinates(series, upperX, upperY, &x, &y);
++
++ if (i == 0) {
++ painterPath.moveTo(x, y);
++ } else {
++ if (i < upper->points().size()
++ && upper->type() == QAbstractSeries::SeriesType::Spline) {
++ qreal x1, y1, x2, y2;
++ calculateRenderCoordinates(series,
++ fittedPoints[j - 1].x(),
++ fittedPoints[j - 1].y(),
++ &x1,
++ &y1);
++ calculateRenderCoordinates(series,
++ fittedPoints[j].x(),
++ fittedPoints[j].y(),
++ &x2,
++ &y2);
++
++ painterPath.cubicTo(x1, y1, x2, y2, x, y);
++ ++j;
++ } else {
++ painterPath.lineTo(x, y);
++ }
++ }
++ }
++ }
++
++ if (lower && series->isVisible()) {
++ Q_TRACE_SCOPE(QGraphs2DAreaRendererCalculateSeriesL);
++ auto &&lowerPoints = lower->points();
++ QList<QPointF> fittedPoints;
++ #if QT_CONFIG(graphs_2d_spline)
++ if (lower->type() == QAbstractSeries::SeriesType::Spline)
++ fittedPoints = qobject_cast<QSplineSeries *>(lower)->getControlPoints();
++ #endif
++
++ for (int i = 0, j = 0; i < lowerPoints.size(); ++i, ++j) {
++ qreal x, y;
++ calculateRenderCoordinates(series,
++ lowerPoints[lowerPoints.size() - 1 - i].x(),
++ lowerPoints[lowerPoints.size() - 1 - i].y(),
++ &x,
++ &y);
++
++ if (i > 0 && lower->type() == QAbstractSeries::SeriesType::Spline) {
++ qreal x1, y1, x2, y2;
++ calculateRenderCoordinates(series,
++ fittedPoints[fittedPoints.size() - 1 - j + 1].x(),
++ fittedPoints[fittedPoints.size() - 1 - j + 1].y(),
++ &x1,
++ &y1);
++ calculateRenderCoordinates(series,
++ fittedPoints[fittedPoints.size() - 1 - j].x(),
++ fittedPoints[fittedPoints.size() - 1 - j].y(),
++ &x2,
++ &y2);
++
++ painterPath.cubicTo(x1, y1, x2, y2, x, y);
++ ++j;
++ } else {
++ painterPath.lineTo(x, y);
++ }
++ }
++
++ qreal x, y;
++ calculateRenderCoordinates(series, upperPoints[0].x(), upperPoints[0].y(), &x, &y);
++ painterPath.lineTo(x, y);
++ }
++
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ if (!m_graph->useCanvasPainter())
++ group->shapePath->setPath(painterPath);
++ #endif
++
++ QList<QLegendData> legendDataList = {{style.color, style.borderColor, series->name()}};
++ series->d_func()->setLegendData(legendDataList);
++ }
++
++ void AreaRenderer::afterPolish(QList<QAbstractSeries *> &cleanupSeries)
++ {
++ Q_TRACE_SCOPE(QGraphs2DAreaRendereAfterPolish);
++ Q_UNUSED(cleanupSeries);
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ auto data = m_shape.data();
++ for (qsizetype i = m_currentShapePathIndex, count = data.count(&data); i < count; ++i) {
++ auto shapePath = qobject_cast<QQuickShapePath *>(data.at(&data, i));
++ if (shapePath) {
++ QPainterPath empty;
++ shapePath->setPath(empty);
++ }
++ }
++ #endif
++ }
++
++ void AreaRenderer::afterUpdate(QList<QAbstractSeries *> &cleanupSeries)
++ {
++ Q_UNUSED(cleanupSeries);
++ }
++
++ void AreaRenderer::updateSeries(QAreaSeries *series)
++ {
++ Q_UNUSED(series);
++ }
++
++ void AreaRenderer::seriesAboutToBeRemoved(QAbstractSeries *series)
++ {
++ if (auto *areaSeries = qobject_cast<QAreaSeries *>(series)) {
++ auto iter = m_groups.find(areaSeries);
++
++ if (iter != m_groups.end()) {
++ #if QT_CONFIG(graphs_2d_high_quality_backend)
++ releasePolarRadialGradient(*iter);
++ #endif
++ delete *iter;
++ m_groups.erase(iter);
++ }
++ }
++ }
++
++ // Point inside triangle code from
++ float sign(QPoint p1, QPoint p2, QPoint p3)
++ {
++ return (p1.x() - p3.x()) * (p2.y() - p3.y()) - (p2.x() - p3.x()) * (p1.y() - p3.y());
++ }
++
++ bool pointInTriangle(QPoint pt, QPoint v1, QPoint v2, QPoint v3)
++ {
++ float d1, d2, d3;
++ bool hasNeg, hasPos;
++
++ d1 = sign(pt, v1, v2);
++ d2 = sign(pt, v2, v3);
++ d3 = sign(pt, v3, v1);
++
++ hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
++ hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
++
++ return !(hasNeg && hasPos);
++ }
++
++ bool AreaRenderer::pointInArea(QPoint pt, QAreaSeries *series) const
++ {
++ Q_TRACE_SCOPE(QGraphs2DAreaRenderePointInArea, pt.x(), pt.y());
++ QList<QPointF> upperPoints = series->upperSeries()->points();
++ QList<QPointF> lowerPoints;
++
++ if (series->lowerSeries())
++ lowerPoints = series->lowerSeries()->points();
++
++ QList<QPointF> *firstPoints = &upperPoints;
++ if (lowerPoints.size() > upperPoints.size())
++ firstPoints = &lowerPoints;
++
++ for (int i = 0; i < firstPoints->size() - 1; ++i) {
++ qreal x1, y1, x2, y2, x3, y3, x4, y4;
++ calculateRenderCoordinates(series, (*firstPoints)[i].x(), (*firstPoints)[i].y(), &x1, &y1);
++ calculateRenderCoordinates(series,
++ (*firstPoints)[i + 1].x(),
++ (*firstPoints)[i + 1].y(),
++ &x2,
++ &y2);
++
++ bool needSecondTriangleTest = true;
++ if (series->lowerSeries()) {
++ QList<QPointF> *secondPoints = &lowerPoints;
++ if (lowerPoints.size() > upperPoints.size())
++ secondPoints = &upperPoints;
++
++ qsizetype firstIndex = i;
++ qsizetype secondIndex = i + 1;
++
++ if (firstIndex >= secondPoints->size())
++ firstIndex = secondPoints->size() - 1;
++ if (secondIndex >= secondPoints->size())
++ needSecondTriangleTest = false;
++
++ calculateRenderCoordinates(series,
++ (*secondPoints)[firstIndex].x(),
++ (*secondPoints)[firstIndex].y(),
++ &x3,
++ &y3);
++
++ if (needSecondTriangleTest) {
++ calculateRenderCoordinates(series,
++ (*secondPoints)[secondIndex].x(),
++ (*secondPoints)[secondIndex].y(),
++ &x4,
++ &y4);
++ } else {
++ x4 = 0.0;
++ y4 = 0.0;
++ }
++ } else {
++ calculateRenderCoordinates(series, upperPoints[i].x(), 0, &x3, &y3);
++ calculateRenderCoordinates(series, upperPoints[i + 1].x(), 0, &x4, &y4);
++ }
++
++ QPoint point1(x1, y1);
++ QPoint point2(x2, y2);
++ QPoint point3(x3, y3);
++ QPoint point4(x4, y4);
++
++ if (pointInTriangle(pt, point1, point2, point3)
++ || (needSecondTriangleTest && pointInTriangle(pt, point2, point3, point4))) {
++ return true;
++ }
++ }
++
++ return false;
++ }
++
++ bool AreaRenderer::handleHoverMove(QHoverEvent *event)
++ {
++ bool handled = false;
++ const QPointF &position = event->position();
++
++ for (auto &&group : m_groups) {
++ if (!group->series->isHoverable() || !group->series->isVisible())
++ continue;
++
++ if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
++ continue;
++
++ if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
++ continue;
++
++ const QString &name = group->series->name();
++
++ bool hovering = false;
++ if (pointInArea(position.toPoint(), group->series)) {
++ qreal x, y;
++ calculateAxisCoordinates(group->series, position.x(), position.y(), &x, &y);
++
++ if (!group->hover) {
++ group->hover = true;
++ group->series->setHovered(true);
++ emit group->series->hoverEnter(name, position, QPointF(x, y));
++ }
++
++ emit group->series->hover(name, position, QPointF(x, y));
++ hovering = true;
++ handled = true;
++ }
++
++ if (!hovering && group->hover) {
++ group->hover = false;
++ group->series->setHovered(false);
++ emit group->series->hoverExit(name, position);
++ handled = true;
++ }
++ }
++ return handled;
++ }
++
++ void AreaRenderer::onSingleTapped(QEventPoint eventPoint, Qt::MouseButton button)
++ {
++ Q_UNUSED(button)
++
++ for (auto &&group : m_groups) {
++ if (!group->series->isSelectable() || !group->series->isVisible())
++ continue;
++
++ if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
++ continue;
++
++ if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
++ continue;
++
++ if (pointInArea(eventPoint.position().toPoint(), group->series)) {
++ group->series->setSelected(!group->series->isSelected());
++ m_graph->polishAndUpdate();
++ qreal x;
++ qreal y;
++ calculateAxisCoordinates(group->series,
++ eventPoint.position().x(),
++ eventPoint.position().y(),
++ &x,
++ &y);
++ emit group->series->clicked(QPoint(x, y));
++ }
++ }
++ }
++
++ void AreaRenderer::onDoubleTapped(QEventPoint eventPoint, Qt::MouseButton button)
++ {
++ Q_UNUSED(button)
++
++ for (auto &&group : m_groups) {
++ if (!group->series->isSelectable() || !group->series->isVisible())
++ continue;
++
++ if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
++ continue;
++
++ if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
++ continue;
++
++ if (pointInArea(eventPoint.position().toPoint(), group->series)) {
++ qreal x;
++ qreal y;
++ calculateAxisCoordinates(group->series,
++ eventPoint.position().x(),
++ eventPoint.position().y(),
++ &x,
++ &y);
++ emit group->series->doubleClicked(QPoint(x, y));
++ }
++ }
++ }
++
++ void AreaRenderer::onPressedChanged()
++ {
++ QPointF position = m_tapHandler->point().position();
++ for (auto &&group : m_groups) {
++ if (!group->series->isSelectable() || !group->series->isVisible())
++ continue;
++
++ if (!group->series->upperSeries() || group->series->upperSeries()->count() < 2)
++ continue;
++
++ if (group->series->lowerSeries() && group->series->lowerSeries()->count() < 2)
++ continue;
++
++ if (pointInArea(position.toPoint(), group->series)) {
++ qreal x;
++ qreal y;
++ calculateAxisCoordinates(group->series, position.x(), position.y(), &x, &y);
++ if (m_tapHandler->isPressed())
++ emit group->series->pressed(QPoint(x, y));
++ else
++ emit group->series->released(QPoint(x, y));
++ }
++ }
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_arearenderer_p.cpp"