// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include PieRenderer::PieRenderer(QGraphsView *graph, bool clipPlotArea) : QQuickItem(graph) , m_graph(graph) , m_painterPath() { setFlag(QQuickItem::ItemHasContents); setClip(clipPlotArea); m_shape = new QQuickShape(this); m_shape->setParentItem(this); m_shape->setPreferredRendererType(QQuickShape::CurveRenderer); m_tapHandler = new QQuickTapHandler(this); connect(m_tapHandler, &QQuickTapHandler::singleTapped, this, &PieRenderer::onSingleTapped); connect(m_tapHandler, &QQuickTapHandler::doubleTapped, this, &PieRenderer::onDoubleTapped); connect(m_tapHandler, &QQuickTapHandler::pressedChanged, this, &PieRenderer::onPressedChanged); } PieRenderer::~PieRenderer() {} void PieRenderer::setSize(QSizeF size) { QQuickItem::setSize(size); } void PieRenderer::handlePolish(QPieSeries *series) { for (QPieSlice *slice : series->slices()) { QPieSlicePrivate *d = slice->d_func(); QQuickShapePath *shapePath = d->m_shapePath; QQuickShapePath *labelPath = d->m_labelPath; auto labelElements = labelPath->pathElements(); auto pathElements = shapePath->pathElements(); auto labelItem = d->m_labelItem; if (!m_activeSlices.contains(slice)) { auto data = m_shape->data(); data.append(&data, shapePath); SliceData sliceData{}; sliceData.initialized = false; m_activeSlices.insert(slice, sliceData); } QQuickShape *labelShape = d->m_labelShape; labelShape->setVisible(series->isVisible() && d->m_isLabelVisible); labelItem->setVisible(series->isVisible() && d->m_isLabelVisible); if (!series->isVisible()) { pathElements.clear(&pathElements); labelElements.clear(&labelElements); continue; } if (!shapePath->parent()) shapePath->setParent(m_shape); if (!d->m_labelItem->parent()) { d->m_labelItem->setParent(this); d->m_labelItem->setParentItem(this); } if (!labelShape->parent()) { labelShape->setParent(this); labelShape->setParentItem(this); } } if (!series->isVisible()) return; QPointF center = QPointF(size().width() * series->horizontalPosition(), size().height() * series->verticalPosition()); qreal radius = size().width() > size().height() ? size().height() : size().width(); radius *= (.5 * series->pieSize()); QGraphsTheme *theme = m_graph->theme(); if (!theme) { qCCritical(lcCritical2D, "Theme not found."); return; } if (m_colorIndex < 0) m_colorIndex = m_graph->graphSeriesCount(); m_graph->setGraphSeriesCount(m_colorIndex + series->slices().size()); qreal sliceAngle = series->startAngle(); int sliceIndex = 0; QList legendDataList; for (QPieSlice *slice : series->slices()) { m_painterPath.clear(); QPieSlicePrivate *d = slice->d_func(); d->setStartAngle(sliceAngle); d->setAngleSpan((series->endAngle() - series->startAngle()) * slice->percentage() * series->valuesMultiplier()); // update slice QQuickShapePath *shapePath = d->m_shapePath; const auto &borderColors = theme->borderColors(); int index = sliceIndex % borderColors.size(); QColor borderColor = borderColors.at(index); if (d->m_borderColor.isValid()) borderColor = d->m_borderColor; qreal borderWidth = theme->borderWidth(); if (d->m_borderWidth >= 1.0) borderWidth = d->m_borderWidth; const auto &seriesColors = theme->seriesColors(); index = sliceIndex % seriesColors.size(); QColor color = seriesColors.at(index); if (d->m_color.isValid()) color = d->m_color; shapePath->setStrokeWidth(borderWidth); shapePath->setStrokeColor(borderColor); shapePath->setFillColor(color); QColor labelTextColor = theme->labelTextColor(); if (d->m_labelColor.isValid()) labelTextColor = d->m_labelColor; d->m_labelItem->setColor(labelTextColor); d->m_labelPath->setStrokeColor(labelTextColor); if (!m_activeSlices.contains(slice)) return; qreal radian = qDegreesToRadians(slice->startAngle()); qreal startBigX = radius * qSin(radian); qreal startBigY = radius * qCos(radian); qreal startSmallX = startBigX * series->holeSize(); qreal startSmallY = startBigY * series->holeSize(); qreal explodeDistance = .0; if (slice->isExploded()) explodeDistance = slice->explodeDistanceFactor() * radius; radian = qDegreesToRadians(slice->startAngle() + (slice->angleSpan() * .5)); qreal xShift = center.x() + (explodeDistance * qSin(radian)); qreal yShift = center.y() - (explodeDistance * qCos(radian)); qreal pointX = startBigY * qSin(radian) + startBigX * qCos(radian); qreal pointY = startBigY * qCos(radian) - startBigX * qSin(radian); QRectF rect(center.x() - radius + (explodeDistance * qSin(radian)), center.y() - radius - (explodeDistance * qCos(radian)), radius * 2, radius * 2); shapePath->setStartX(center.x()); shapePath->setStartY(center.y()); if (series->holeSize() > 0) { QRectF insideRect(center.x() - series->holeSize() * radius + (explodeDistance * qSin(radian)), center.y() - series->holeSize() * radius - (explodeDistance * qCos(radian)), series->holeSize() * radius * 2, series->holeSize() * radius * 2); m_painterPath.arcMoveTo(rect, -slice->startAngle() + 90); m_painterPath.arcTo(rect, -slice->startAngle() + 90, -slice->angleSpan()); m_painterPath.arcTo(insideRect, -slice->startAngle() + 90 - slice->angleSpan(), slice->angleSpan()); m_painterPath.closeSubpath(); } else { m_painterPath.moveTo(rect.center()); m_painterPath.arcTo(rect, -slice->startAngle() + 90, -slice->angleSpan()); m_painterPath.closeSubpath(); } radian = qDegreesToRadians(slice->angleSpan()); pointX = startSmallY * qSin(radian) + startSmallX * qCos(radian); pointY = startSmallY * qCos(radian) - startSmallX * qSin(radian); d->m_largeArc = {xShift + pointX, yShift - pointY}; shapePath->setPath(m_painterPath); m_painterPath.clear(); radian = qDegreesToRadians(slice->startAngle() + (slice->angleSpan() * .5)); startBigX = radius * qSin(radian); startBigY = radius * qCos(radian); pointX = radius * (1.0 + d->m_labelArmLengthFactor) * qSin(radian); pointY = radius * (1.0 + d->m_labelArmLengthFactor) * qCos(radian); m_painterPath.moveTo(xShift + startBigX, yShift - startBigY); m_painterPath.lineTo(xShift + pointX, yShift - pointY); d->m_centerLine = {xShift + pointX, yShift - pointY}; d->m_labelArm = {xShift + pointX, yShift - pointY}; auto labelWidth = radian > M_PI ? -d->m_labelItem->width() : d->m_labelItem->width(); m_painterPath.lineTo(d->m_labelArm.x() + labelWidth, d->m_labelArm.y()); d->setLabelPosition(d->m_labelPosition); d->m_labelPath->setPath(m_painterPath); sliceAngle += slice->angleSpan(); sliceIndex++; legendDataList.push_back({color, borderColor, d->m_labelText}); } series->d_func()->setLegendData(legendDataList); } void PieRenderer::afterPolish(QList &cleanupSeries) { for (auto series : cleanupSeries) { auto pieSeries = qobject_cast(series); if (pieSeries) { for (QPieSlice *slice : pieSeries->slices()) { QPieSlicePrivate *d = slice->d_func(); auto labelElements = d->m_labelPath->pathElements(); auto shapeElements = d->m_shapePath->pathElements(); labelElements.clear(&labelElements); shapeElements.clear(&shapeElements); slice->deleteLater(); d->m_labelItem->deleteLater(); m_activeSlices.remove(slice); } } } } void PieRenderer::updateSeries(QPieSeries *series) { auto needPolish = false; for (auto &sliceData : m_activeSlices) { if (!sliceData.initialized) { sliceData.initialized = true; needPolish = true; } } if (needPolish) handlePolish(series); } void PieRenderer::afterUpdate(QList &cleanupSeries) { Q_UNUSED(cleanupSeries); } void PieRenderer::markedDeleted(QList deleted) { auto emptyPath = QPainterPath{}; for (auto slice : deleted) { auto d = slice->d_func(); d->m_shapePath->setPath(emptyPath); d->m_labelPath->setPath(emptyPath); d->m_labelItem->deleteLater(); m_activeSlices.remove(slice); } } bool PieRenderer::isPointInSlice(QPointF point, QPieSlice *slice, qreal *angle) { QPieSeries* series = slice->series(); QPointF center = QPointF(size().width() * series->horizontalPosition(), size().height() * series->verticalPosition()); qreal radius = size().width() > size().height() ? size().height() : size().width(); radius *= (.5 * series->pieSize()); qreal explodeDistance = .0; if (slice->isExploded()) explodeDistance = slice->explodeDistanceFactor() * radius; qreal radian = qDegreesToRadians(slice->startAngle() + (slice->angleSpan() * .5)); qreal xShift = center.x() + (explodeDistance * qSin(radian)); qreal yShift = center.y() - (explodeDistance * qCos(radian)); QPointF adjustedPosition = QPointF(point.x() - xShift, point.y() - yShift); qreal distance = qSqrt(qPow(adjustedPosition.x(), 2) + qPow(adjustedPosition.y(), 2)); qreal foundAngle = qRadiansToDegrees(qAtan2(adjustedPosition.y(), adjustedPosition.x())) + 90; if (foundAngle < 0) foundAngle += 360; if (angle) *angle = foundAngle; if (distance <= radius && foundAngle >= slice->startAngle() && foundAngle <= (slice->startAngle() + slice->angleSpan())) { return true; } return false; } bool PieRenderer::handleHoverMove(QHoverEvent *event) { bool handled = false; const QPointF &position = event->position(); bool hovering = false; QList list = m_activeSlices.keys(); for (const auto &slice : list) { if (!slice->series()->isHoverable()) continue; qreal angle; if (isPointInSlice(position, slice, &angle)) { const QString &name = slice->series()->name(); const QPointF value(slice->startAngle(), angle); if (!m_currentHoverSlice) { m_currentHoverSlice = slice; slice->series()->setHovered(true); emit slice->series()->hoverEnter(name, position, value); } if (m_currentHoverSlice != slice) { slice->series()->setHovered(true); emit m_currentHoverSlice->series()->hoverExit(name, position); emit slice->series()->hoverEnter(name, position, value); m_currentHoverSlice = slice; } emit slice->series()->hover(name, position, value); hovering = true; handled = true; } } if (!hovering && m_currentHoverSlice) { m_currentHoverSlice->series()->setHovered(false); emit m_currentHoverSlice->series()-> hoverExit(m_currentHoverSlice->series()->name(), position); m_currentHoverSlice = nullptr; handled = true; } return handled; } void PieRenderer::onSingleTapped(QEventPoint eventPoint, Qt::MouseButton button) { Q_UNUSED(button) QList list = m_activeSlices.keys(); for (const auto &pieSlice : list) { if (!pieSlice->series()->isSelectable()) continue; if (isPointInSlice(eventPoint.position(), pieSlice)) { emit pieSlice->series()->clicked(pieSlice); return; } } } void PieRenderer::onDoubleTapped(QEventPoint eventPoint, Qt::MouseButton button) { Q_UNUSED(button) QList list = m_activeSlices.keys(); for (const auto &pieSlice : list) { if (!pieSlice->series()->isSelectable()) continue; if (isPointInSlice(eventPoint.position(), pieSlice)) { emit pieSlice->series()->doubleClicked(pieSlice); return; } } } void PieRenderer::onPressedChanged() { QList list = m_activeSlices.keys(); for (const auto &pieSlice : list) { if (!pieSlice->series()->isSelectable()) continue; if (isPointInSlice(m_tapHandler->point().position(), pieSlice)) { if (m_tapHandler->isPressed()) emit pieSlice->series()->pressed(pieSlice); else emit pieSlice->series()->released(pieSlice); return; } } }