From 207ba079a17180124841442587e723fe6a8914c9 Mon Sep 17 00:00:00 2001 From: Uwe Rathmann Date: Thu, 20 Apr 2023 11:15:46 +0200 Subject: [PATCH] charts code added: will become Qsk classes at some point --- playground/CMakeLists.txt | 1 + playground/charts/CMakeLists.txt | 12 + playground/charts/ChartSample.cpp | 35 ++ playground/charts/ChartSample.h | 110 +++++ playground/charts/ChartView.cpp | 229 +++++++++++ playground/charts/ChartView.h | 16 + playground/charts/CircularChart.cpp | 132 ++++++ playground/charts/CircularChart.h | 57 +++ playground/charts/CircularChartSkinlet.cpp | 454 +++++++++++++++++++++ playground/charts/CircularChartSkinlet.h | 61 +++ playground/charts/StackedChart.cpp | 112 +++++ playground/charts/StackedChart.h | 60 +++ playground/charts/main.cpp | 79 ++++ 13 files changed, 1358 insertions(+) create mode 100644 playground/charts/CMakeLists.txt create mode 100644 playground/charts/ChartSample.cpp create mode 100644 playground/charts/ChartSample.h create mode 100644 playground/charts/ChartView.cpp create mode 100644 playground/charts/ChartView.h create mode 100644 playground/charts/CircularChart.cpp create mode 100644 playground/charts/CircularChart.h create mode 100644 playground/charts/CircularChartSkinlet.cpp create mode 100644 playground/charts/CircularChartSkinlet.h create mode 100644 playground/charts/StackedChart.cpp create mode 100644 playground/charts/StackedChart.h create mode 100644 playground/charts/main.cpp diff --git a/playground/CMakeLists.txt b/playground/CMakeLists.txt index 4dab3e0a..7fc3449a 100644 --- a/playground/CMakeLists.txt +++ b/playground/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(gradients) add_subdirectory(invoker) add_subdirectory(shadows) add_subdirectory(shapes) +add_subdirectory(charts) if (BUILD_INPUTCONTEXT) add_subdirectory(inputpanel) diff --git a/playground/charts/CMakeLists.txt b/playground/charts/CMakeLists.txt new file mode 100644 index 00000000..bcbd2f24 --- /dev/null +++ b/playground/charts/CMakeLists.txt @@ -0,0 +1,12 @@ +############################################################################ +# QSkinny - Copyright (C) 2016 Uwe Rathmann +# This file may be used under the terms of the 3-clause BSD License +############################################################################ + +qsk_add_example(charts + ChartSample.h ChartSample.cpp + CircularChartSkinlet.h CircularChartSkinlet.cpp + StackedChart.h StackedChart.cpp + CircularChart.h CircularChart.cpp + ChartView.h ChartView.cpp + main.cpp ) diff --git a/playground/charts/ChartSample.cpp b/playground/charts/ChartSample.cpp new file mode 100644 index 00000000..f2e6a4f6 --- /dev/null +++ b/playground/charts/ChartSample.cpp @@ -0,0 +1,35 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "ChartSample.h" + +static void qskRegisterChartSample() +{ + qRegisterMetaType< ChartSample >(); + +#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) + QMetaType::registerEqualsComparator< ChartSample >(); +#endif +} + +Q_CONSTRUCTOR_FUNCTION( qskRegisterChartSample ) + +#ifndef QT_NO_DEBUG_STREAM + +#include + +QDebug operator<<( QDebug debug, const ChartSample& sample ) +{ + QDebugStateSaver saver( debug ); + debug.nospace(); + + debug << "ChartSample" << "( " << sample.title() << ": " + << sample.value() << sample.gradient() << " )"; + return debug; +} + +#endif + +#include "moc_ChartSample.cpp" diff --git a/playground/charts/ChartSample.h b/playground/charts/ChartSample.h new file mode 100644 index 00000000..0f1c9f1d --- /dev/null +++ b/playground/charts/ChartSample.h @@ -0,0 +1,110 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +class ChartSample +{ + Q_GADGET + + Q_PROPERTY( QString title READ title WRITE setTitle ) + Q_PROPERTY( qreal value READ value WRITE setValue ) + Q_PROPERTY( QskGradient gradient READ gradient WRITE setGradient ) + + public: + ChartSample() noexcept = default; + ChartSample( const QString&, qreal, const QskGradient& ) noexcept; + + void setSample( const QString&, qreal, const QskGradient& ) noexcept; + + bool operator==( const ChartSample& ) const noexcept; + bool operator!=( const ChartSample& ) const noexcept; + + QString title() const noexcept; + void setTitle( const QString& ) noexcept; + + qreal value() const noexcept; + void setValue( qreal ) noexcept; + + QskGradient gradient() const noexcept; + void setGradient( const QskGradient& ) noexcept; + + private: + QString m_title; + qreal m_value = 0.0; + QskGradient m_gradient; +}; + +Q_DECLARE_METATYPE( ChartSample ) + +inline ChartSample::ChartSample( const QString& title, qreal value, + const QskGradient& gradient ) noexcept + : m_title( title ) + , m_value( value ) + , m_gradient( gradient ) +{ +} + +inline bool ChartSample::operator==( const ChartSample& other ) const noexcept +{ + return ( m_value == other.m_value ) + && ( m_title == other.m_title ) + && ( m_gradient == other.m_gradient ); +} + +inline bool ChartSample::operator!=( const ChartSample& other ) const noexcept +{ + return ( !( *this == other ) ); +} + +inline QString ChartSample::title() const noexcept +{ + return m_title; +} + +inline void ChartSample::setTitle( const QString& title ) noexcept +{ + m_title = title; +} + +inline qreal ChartSample::value() const noexcept +{ + return m_value; +} + +inline void ChartSample::setValue( qreal value ) noexcept +{ + m_value = value; +} + +inline QskGradient ChartSample::gradient() const noexcept +{ + return m_gradient; +} + +inline void ChartSample::setGradient( const QskGradient& gradient ) noexcept +{ + m_gradient = gradient; +} + +inline void ChartSample::setSample( + const QString& title, qreal value, const QskGradient& gradient ) noexcept +{ + m_title = title; + m_value = value; + m_gradient = gradient; +} + +#ifndef QT_NO_DEBUG_STREAM + + class QDebug; + QSK_EXPORT QDebug operator<<( QDebug, const ChartSample& ); + +#endif diff --git a/playground/charts/ChartView.cpp b/playground/charts/ChartView.cpp new file mode 100644 index 00000000..364846cf --- /dev/null +++ b/playground/charts/ChartView.cpp @@ -0,0 +1,229 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "ChartView.h" +#include "ChartSample.h" +#include "CircularChart.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + class ChartBox : public QskControl + { + Q_OBJECT + + public: + ChartBox( CircularChart* chart, QQuickItem* parent = nullptr ) + : QskControl( parent ) + , m_chart( chart ) + { + m_chart->setParentItem( this ); + if ( m_chart->parent() == nullptr ) + m_chart->setParent( this ); + + setPolishOnResize( true ); + setMargins( 10 ); + } + + QskArcMetrics arcMetrics() const + { + return m_chart->arcMetrics(); + } + + public Q_SLOTS: + void setThickness( qreal thickness ) + { + m_chart->setArcThickness( thickness, Qt::RelativeSize ); + } + + void setStartAngle( qreal degrees ) + { + auto metrics = m_chart->arcMetrics(); + metrics.setStartAngle( degrees ); + m_chart->setArcMetrics( metrics ); + } + + void setSpanAngle( qreal degrees ) + { + auto metrics = m_chart->arcMetrics(); + metrics.setSpanAngle( degrees ); + m_chart->setArcMetrics( metrics ); + + polish(); + } + + protected: + void updateLayout() override + { + const auto r = layoutRect(); + + m_chart->setArcDiameters( r.size() ); + + const auto align = Qt::AlignTop | Qt::AlignHCenter; + m_chart->setGeometry( qskAlignedRectF( r, m_chart->sizeHint(), align ) ); + } + + private: + CircularChart* m_chart = nullptr; + }; +} + +namespace +{ + class SliderBox : public QskLinearBox + { + Q_OBJECT + + public: + SliderBox( const QString& label, qreal min, qreal max, qreal value ) + { + auto textLabel = new QskTextLabel( label, this ); + textLabel->setSizePolicy( Qt::Horizontal, QskSizePolicy::Fixed ); + + auto slider = new QskSlider( this ); + slider->setBoundaries( min, max ); + slider->setValue( value ); + slider->setStepSize( 1.0 ); + slider->setPageSize( 10.0 ); + + connect( slider, &QskSlider::valueChanged, + this, &SliderBox::valueChanged ); + } + + Q_SIGNALS: + void valueChanged( qreal ); + }; +} + +namespace +{ + class ControlPanel : public QskGridBox + { + Q_OBJECT + + public: + ControlPanel( const QskArcMetrics& metrics, QQuickItem* parent = nullptr ) + : QskGridBox( parent ) + { + setPanel( true ); + setPaddingHint( QskBox::Panel, 20 ); + setSpacing( 10 ); + + auto sliderStart = new SliderBox( "Angle", 0.0, 360.0, metrics.startAngle() ); + auto sliderSpan = new SliderBox( "Span", -360.0, 360.0, metrics.spanAngle() ); + auto sliderExtent = new SliderBox( "Extent", 10.0, 100.0, metrics.thickness() ); + + connect( sliderStart, &SliderBox::valueChanged, + this, &ControlPanel::startAngleChanged ); + + connect( sliderSpan, &SliderBox::valueChanged, + this, &ControlPanel::spanAngleChanged ); + + connect( sliderExtent, &SliderBox::valueChanged, + this, &ControlPanel::thicknessChanged ); + + addItem( sliderStart, 0, 0 ); + addItem( sliderExtent, 0, 1 ); + addItem( sliderSpan, 1, 0, 1, 2 ); + } + + Q_SIGNALS: + void thicknessChanged( qreal ); + void startAngleChanged( qreal ); + void spanAngleChanged( qreal ); + }; + + class Legend : public QskGridBox + { + public: + Legend( QQuickItem* parent = nullptr ) + : QskGridBox( parent ) + { + setMargins( 10 ); + setLayoutAlignmentHint( Qt::AlignLeft | Qt::AlignTop ); + } + + void setSamples( const QVector< ChartSample >& samples ) + { + clear( true ); + + for ( const auto& sample : samples ) + { + // using QskBox instead TODO ... + auto iconLabel = new QskGraphicLabel( graphic( sample.gradient() ) ); + iconLabel->setSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed ); + + auto textLabel = new QskTextLabel( sample.title() ); + textLabel->setAlignment( Qt::AlignLeft | Qt::AlignVCenter ); + + const auto row = rowCount(); + + addItem( iconLabel, row, 0 ); + addItem( textLabel, row, 1 ); + } + } + + private: + QskGraphic graphic( const QskGradient& gradient ) const + { + QskGraphic identifier; + + QPainter painter( &identifier ); + painter.setPen( QPen( QskRgb::toTransparent( Qt::black, 100 ), 1 ) ); + painter.setBrush( gradient.toQGradient() ); + + QLinearGradient qGradient; + qGradient.setStops( qskToQGradientStops( gradient.stops() ) ); + painter.setBrush( qGradient ); + + painter.drawRect( 0, 0, 20, 20 ); + painter.end(); + + return identifier; + } + }; + +} + +ChartView::ChartView( CircularChart* chart, QQuickItem* parent ) + : QskMainView( parent ) +{ + auto hBox = new QskLinearBox( Qt::Horizontal ); + + auto chartBox = new ChartBox( chart, hBox ); + + auto legend = new Legend( hBox ); + legend->setSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed ); + legend->setSamples( chart->series() ); + + auto controlPanel = new ControlPanel( chartBox->arcMetrics() ); + controlPanel->setSizePolicy( Qt::Vertical, QskSizePolicy::Fixed ); + + connect( controlPanel, &ControlPanel::thicknessChanged, + chartBox, &ChartBox::setThickness ); + + connect( controlPanel, &ControlPanel::startAngleChanged, + chartBox, &ChartBox::setStartAngle ); + + connect( controlPanel, &ControlPanel::spanAngleChanged, + chartBox, &ChartBox::setSpanAngle ); + + setHeader( controlPanel ); + setBody( hBox ); +} + +#include "ChartView.moc" diff --git a/playground/charts/ChartView.h b/playground/charts/ChartView.h new file mode 100644 index 00000000..808c6bd6 --- /dev/null +++ b/playground/charts/ChartView.h @@ -0,0 +1,16 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include + +class CircularChart; + +class ChartView : public QskMainView +{ + public: + ChartView( CircularChart*, QQuickItem* parent = nullptr ); +}; diff --git a/playground/charts/CircularChart.cpp b/playground/charts/CircularChart.cpp new file mode 100644 index 00000000..6c4730e0 --- /dev/null +++ b/playground/charts/CircularChart.cpp @@ -0,0 +1,132 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "CircularChart.h" +#include "CircularChartSkinlet.h" +#include "ChartSample.h" + +#include +#include +#include + +QSK_SUBCONTROL( CircularChart, Panel ) +QSK_SUBCONTROL( CircularChart, Arc ) +QSK_SUBCONTROL( CircularChart, Segment ) +QSK_SUBCONTROL( CircularChart, SegmentLabel ) + +CircularChart::CircularChart( QQuickItem* parentItem ) + : StackedChart( parentItem ) +{ +#if 1 + /* + This code has to go to the skins, but for the moment + we do a local customization + */ + setGradientHint( Panel, QskGradient() ); + setBoxBorderMetricsHint( Panel, 0 ); + + setArcMetricsHint( Arc, { 90.0, -360.0, 100.0, Qt::RelativeSize } ); + + setGradientHint( Arc, QskRgb::toTransparent( QskRgb::LightGray, 100 ) ); + setColor( Arc | QskAspect::Border, QskRgb::LightGray ); + setMetric( Arc | QskAspect::Border, 1.0 ); + + setColor( Segment | QskAspect::Border, QskRgb::Black ); + setMetric( Segment | QskAspect::Border, 1.0 ); + + auto skinlet = new CircularChartSkinlet(); + skinlet->setOwnedBySkinnable( true ); + + setSkinlet( skinlet ); +#endif +} + +CircularChart::~CircularChart() +{ +} + +QSizeF CircularChart::arcDiameters() const +{ + return strutSizeHint( Arc ); +} + +void CircularChart::setArcDiameter( qreal diameter ) +{ + setArcDiameters( QSizeF( diameter, diameter ) ); +} + +void CircularChart::setArcDiameters( qreal diameterX, qreal diameterY ) +{ + setArcDiameters( QSizeF( diameterX, diameterY ) ); +} + +void CircularChart::setArcDiameters( const QSizeF& diameters ) +{ + if ( setStrutSizeHint( Arc, diameters ) ) + { + resetImplicitSize(); + update(); + + Q_EMIT arcDiametersChanged( diameters ); + } +} + +void CircularChart::resetArcDiameters() +{ + if ( resetStrutSizeHint( Arc ) ) + { + resetImplicitSize(); + update(); + + Q_EMIT arcDiametersChanged( arcDiameters() ); + } +} + +QskArcMetrics CircularChart::arcMetrics() const +{ + return arcMetricsHint( Arc ); +} + +void CircularChart::setArcMetrics( const QskArcMetrics& metrics ) +{ + if ( setArcMetricsHint( Arc, metrics ) ) + { + resetImplicitSize(); + update(); + + Q_EMIT arcMetricsChanged( metrics ); + } +} + +void CircularChart::resetArcMetrics() +{ + if ( resetArcMetricsHint( Arc ) ) + { + resetImplicitSize(); + update(); + + Q_EMIT arcMetricsChanged( arcMetrics() ); + } +} + +void CircularChart::setArcThickness( qreal width, Qt::SizeMode sizeMode ) +{ + auto metrics = arcMetricsHint( Arc ); + metrics.setThickness( width ); + metrics.setSizeMode( sizeMode ); + + setArcMetrics( metrics ); +} + +void CircularChart::setArcAngles( qreal startAngle, qreal spanAngle ) +{ + auto metrics = arcMetricsHint( Arc ); + metrics.setStartAngle( startAngle ); + metrics.setSpanAngle( spanAngle ); + + setArcMetrics( metrics ); +} + +#include "moc_CircularChart.cpp" diff --git a/playground/charts/CircularChart.h b/playground/charts/CircularChart.h new file mode 100644 index 00000000..d51106be --- /dev/null +++ b/playground/charts/CircularChart.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "StackedChart.h" + +class QskArcMetrics; + +/* + Pie/Donut chart + + The default setting organizes the values clockwise in a + full circle starting at 90°. + + A chart where the thickness of the arc is smaller than the radius + is usually referred as "donut" ( or "doughnut" ) chart while it is + called a "pie" chart otherwise. + */ +class CircularChart : public StackedChart +{ + Q_OBJECT + + Q_PROPERTY( QskArcMetrics arcMetrics READ arcMetrics + WRITE setArcMetrics RESET resetArcMetrics NOTIFY arcMetricsChanged ) + + Q_PROPERTY( QSizeF arcDiameters READ arcDiameters + WRITE setArcDiameters RESET resetArcDiameters NOTIFY arcDiametersChanged ) + + using Inherited = StackedChart; + + public: + QSK_SUBCONTROLS( Panel, Arc, Segment, SegmentLabel ) + + CircularChart( QQuickItem* parent = nullptr ); + ~CircularChart() override; + + QskArcMetrics arcMetrics() const; + void resetArcMetrics(); + + QSizeF arcDiameters() const; + void setArcDiameters( qreal width, qreal height ); + void resetArcDiameters(); + + public Q_SLOTS: + void setArcDiameters( const QSizeF& ); + void setArcDiameter( qreal size ); + void setArcThickness( qreal width, Qt::SizeMode = Qt::RelativeSize ); + void setArcMetrics( const QskArcMetrics& ); + void setArcAngles( qreal startAngle, qreal spanAngle ); + + Q_SIGNALS: + void arcMetricsChanged( const QskArcMetrics& ); + void arcDiametersChanged( const QSizeF& ); +}; diff --git a/playground/charts/CircularChartSkinlet.cpp b/playground/charts/CircularChartSkinlet.cpp new file mode 100644 index 00000000..86af9634 --- /dev/null +++ b/playground/charts/CircularChartSkinlet.cpp @@ -0,0 +1,454 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "CircularChartSkinlet.h" +#include "CircularChart.h" +#include "ChartSample.h" + +#include +#include +#include + +#include +#include + +#define PAINTED_NODE 0 + +#if PAINTED_NODE + +/* + This is a fallback implementation for a user who is using an outdated + version of QSkinny where only shaders for linear gradients are available. + */ +#include +#include +#include +#include +#include + +namespace +{ + class PaintedArcNode : public QskPaintedNode + { + public: + void setArcData( const QRectF&, const QskArcMetrics&, + qreal, const QColor&, const QskGradient&, QQuickWindow* ); + + protected: + void paint( QPainter*, const QSize&, const void* nodeData ) override; + QskHashValue hash( const void* nodeData ) const override; + + private: + QskHashValue arcHash( const QRectF&, const QskArcMetrics&, + qreal, const QColor&, const QskGradient& ) const; + + QBrush fillBrush( const QskGradient&, const QRectF&, qreal, qreal ) const; + + struct ArcData + { + QPointF translation; + QPen pen; + QBrush brush; + QPainterPath path; + + QskHashValue hash; + }; + }; + + void PaintedArcNode::setArcData( + const QRectF& rect, const QskArcMetrics& metrics, + qreal borderWidth, const QColor& borderColor, + const QskGradient& gradient, QQuickWindow* window ) + { + const auto hash = arcHash( rect, metrics, borderWidth, borderColor, gradient ); + + const auto brush = fillBrush( gradient, rect, + metrics.startAngle(), metrics.spanAngle() ); + + QPen pen( borderColor, borderWidth ); + if ( borderWidth <= 0.0 ) + pen.setStyle( Qt::NoPen ); + + const auto path = metrics.painterPath( rect ); + const auto r = path.controlPointRect(); + + const ArcData arcData { r.topLeft(), pen, brush, path, hash }; + update( window, r, QSizeF(), &arcData ); + } + + void PaintedArcNode::paint( QPainter* painter, const QSize&, const void* nodeData ) + { + const auto arcData = reinterpret_cast< const ArcData* >( nodeData ); + + painter->setRenderHint( QPainter::Antialiasing, true ); + painter->translate( -arcData->translation ); + painter->setPen( arcData->pen ); + painter->setBrush( arcData->brush ); + painter->drawPath( arcData->path ); + } + + QskHashValue PaintedArcNode::hash( const void* nodeData ) const + { + const auto arcData = reinterpret_cast< const ArcData* >( nodeData ); + return arcData->hash; + } + + QBrush PaintedArcNode::fillBrush( const QskGradient& gradient, + const QRectF& rect, qreal startAngle, qreal spanAngle ) const + { + const auto stops = gradient.stops(); + + QskGradientStops scaledStops; + scaledStops.reserve( gradient.stops().size() ); + + const auto ratio = qAbs( spanAngle ) / 360.0; + + if ( spanAngle > 0.0 ) + { + for ( auto it = stops.cbegin(); it != stops.cend(); ++it ) + scaledStops += { ratio* it->position(), it->color() }; + } + else + { + for ( auto it = stops.crbegin(); it != stops.crend(); ++it ) + scaledStops += { 1.0 - ratio * it->position(), it->color() }; + } + + QConicalGradient qGradient( QPointF(), startAngle ); + qGradient.setStops( qskToQGradientStops( scaledStops ) ); + + const qreal sz = qMax( rect.width(), rect.height() ); + const qreal sx = rect.width() / sz; + const qreal sy = rect.height() / sz; + + QTransform t; + t.scale( sx, sy ); + t.translate( rect.center().x() / sx, rect.center().y() / sy ); + + QBrush brush( qGradient ); + brush.setTransform( t ); + + return brush; + } + + inline QskHashValue PaintedArcNode::arcHash( + const QRectF& rect, const QskArcMetrics& metrics, qreal borderWidth, + const QColor& borderColor, const QskGradient& gradient ) const + { + auto hash = metrics.hash( 6753 ); + hash = qHashBits( &rect, sizeof( rect ), hash ); + hash = qHash( borderWidth, hash ); + hash = qHash( borderColor.rgba(), hash ); + hash = gradient.hash( hash ); + + return hash; + } +} + +#endif // PAINTED_NODE + +namespace +{ + inline QskArcMetrics segmentMetrics( + const QskSkinnable* skinnable, int index ) + { + auto metrics = skinnable->arcMetricsHint( CircularChart::Arc ); + + const auto chart = static_cast< const CircularChart* >( skinnable ); + + const auto span = chart->stackedInterval( index ); + const auto total = chart->effectiveTotalValue(); + + const auto a1 = metrics.angleAtRatio( span.lowerBound() / total ); + const auto a2 = metrics.angleAtRatio( span.upperBound() / total ); + + metrics.setStartAngle( a1 ); + metrics.setSpanAngle( a2 - a1 ); + + return metrics; + } + + QRectF closedArcRect( const QskSkinnable* skinnable ) + { + using Q = CircularChart; + + const auto chart = static_cast< const CircularChart* >( skinnable ); + const auto r = chart->subControlContentsRect( Q::Arc ); + + const auto metrics = skinnable->arcMetricsHint( Q::Arc ); + if ( metrics.isClosed() ) + return r; + + const auto size = skinnable->strutSizeHint( CircularChart::Arc ); + + auto pos = r.topLeft(); + + { + const QRectF ellipseRect( 0.0, 0.0, size.width(), size.height() ); + pos -= metrics.boundingRect( ellipseRect ).topLeft(); + } + + return QRectF( pos.x(), pos.y(), size.width(), size.height() ); + } + + static inline QRectF textRect( qreal x, qreal y ) + { + const qreal sz = 1000.0; // something big enough to avoid wrapping + return QRectF( x - 0.5 * sz, y - 0.5 * sz, sz, sz ); + } +} + +class CircularChartSkinlet::PrivateData +{ + public: + // caching the geometry of the rect for drawing all arc segments + mutable QRectF closedArcRect; +}; + +CircularChartSkinlet::CircularChartSkinlet( QskSkin* skin ) + : Inherited( skin ) + , m_data( new PrivateData() ) +{ + setNodeRoles( { PanelRole, ArcRole, SegmentRole, SegmentLabelRole } ); +} + +CircularChartSkinlet::~CircularChartSkinlet() = default; + +void CircularChartSkinlet::updateNode( + QskSkinnable* skinnable, QSGNode* node ) const +{ + m_data->closedArcRect = ::closedArcRect( skinnable ); + Inherited::updateNode( skinnable, node ); +} + +QRectF CircularChartSkinlet::subControlRect( const QskSkinnable* skinnable, + const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const +{ + using Q = CircularChart; + + if ( subControl == Q::Panel ) + return contentsRect; + + if ( subControl == Q::Arc ) + { + const auto chart = static_cast< const CircularChart* >( skinnable ); + return chart->subControlContentsRect( Q::Panel ); + } + + return Inherited::subControlRect( skinnable, contentsRect, subControl ); +} + +QSGNode* CircularChartSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + using Q = CircularChart; + + switch ( nodeRole ) + { + case PanelRole: + return updateBoxNode( skinnable, node, Q::Panel ); + + case ArcRole: + { + const auto aspect = Q::Arc | QskAspect::Border; + + const auto borderColor = skinnable->color( aspect ); + const auto borderWidth = skinnable->metric( aspect ); + + return updateArcSegmentNode( skinnable, node, borderWidth, borderColor, + skinnable->gradientHint( Q::Arc ), skinnable->arcMetricsHint( Q::Arc ) ); + } + + case SegmentRole: + return updateSeriesNode( skinnable, Q::Segment, node ); + + case SegmentLabelRole: + return updateSeriesNode( skinnable, Q::SegmentLabel, node ); + } + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +int CircularChartSkinlet::sampleCount( + const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const +{ + using Q = CircularChart; + + if ( subControl == Q::Segment || subControl == Q::SegmentLabel ) + { + const auto chart = static_cast< const CircularChart* >( skinnable ); + return chart->series().count(); + } + + return Inherited::sampleCount( skinnable, subControl ); +} + +QRectF CircularChartSkinlet::sampleRect( const QskSkinnable* skinnable, + const QRectF& contentsRect, QskAspect::Subcontrol subControl, int index ) const +{ + // See https://github.com/uwerat/qskinny/issues/307 + return Inherited::sampleRect( skinnable, contentsRect, subControl, index ); +} + +int CircularChartSkinlet::sampleIndexAt( + const QskSkinnable* skinnable, const QRectF& contentsRect, + QskAspect::Subcontrol subControl, const QPointF& pos ) const +{ + // See https://github.com/uwerat/qskinny/issues/307 + return Inherited::sampleIndexAt( skinnable, contentsRect, subControl, pos ); +} + +QSGNode* CircularChartSkinlet::updateSampleNode( const QskSkinnable* skinnable, + QskAspect::Subcontrol subControl, int index, QSGNode* node ) const +{ + using Q = CircularChart; + + const auto chart = static_cast< const CircularChart* >( skinnable ); + + if ( subControl == Q::Segment ) + { + const auto aspect = Q::Segment | QskAspect::Border; + + const auto borderColor = skinnable->color( aspect ); + const auto borderWidth = skinnable->metric( aspect ); + + return updateArcSegmentNode( skinnable, node, borderWidth, borderColor, + chart->gradientAt( index ), ::segmentMetrics( skinnable, index ) ); + } + + if ( subControl == Q::SegmentLabel ) + { + return updateArcLabelNode( skinnable, node, + chart->labelAt( index ), ::segmentMetrics( skinnable, index ) ); + } + + return nullptr; +} + +QSGNode* CircularChartSkinlet::updateArcSegmentNode( + const QskSkinnable* skinnable, + QSGNode* node, qreal borderWidth, const QColor& borderColor, + const QskGradient& gradient, const QskArcMetrics& metrics ) const +{ + auto fillGradient = gradient; + + if ( fillGradient.type() == QskGradient::Stops ) + { + fillGradient.setStretchMode( QskGradient::StretchToSize ); + fillGradient.setConicDirection( 0.5, 0.5, + metrics.startAngle(), metrics.spanAngle() ); + } + +#if PAINTED_NODE + auto arcNode = static_cast< PaintedArcNode* >( node ); + if ( arcNode == nullptr ) + arcNode = new PaintedArcNode(); + + const auto chart = static_cast< const CircularChart* >( skinnable ); + + arcNode->setArcData( m_data->closedArcRect, metrics, + borderWidth, borderColor, fillGradient, chart->window() ); +#else + Q_UNUSED( skinnable ) + + auto arcNode = static_cast< QskArcNode* >( node ); + if ( arcNode == nullptr ) + arcNode = new QskArcNode(); + + arcNode->setArcData( m_data->closedArcRect, metrics, + borderWidth, borderColor, fillGradient ); +#endif + + return arcNode; +} + +QSGNode* CircularChartSkinlet::updateArcLabelNode( + const QskSkinnable* skinnable, QSGNode* node, + const QString& text, const QskArcMetrics& metrics ) const +{ + /* + possible improvements: + + - rotating the labels + - elide/wrap modes + */ + const auto& r = m_data->closedArcRect; + const auto sz = qMin( r.width(), r.height() ); + + if ( text.isEmpty() || sz <= 0.0 ) + return nullptr; + + const auto m = metrics.toAbsolute( 0.5 * sz ); + + const auto pos = 0.5; // [0.0, 1.0] -> distance from the inner border + const auto dt = pos * ( 1.0 - m.thickness() / sz ); + + const qreal radians = qDegreesToRadians( m.startAngle() + 0.5 * m.spanAngle() ); + const auto c = r.center(); + + const qreal x = c.x () + dt * r.width() * qFastCos( radians ); + const qreal y = c.y() - dt * r.height() * qFastSin( radians ); + + return updateTextNode( skinnable, node, + textRect( x, y ), Qt::AlignCenter, text, CircularChart::SegmentLabel ); +} + +QSizeF CircularChartSkinlet::sizeHint( const QskSkinnable* skinnable, + Qt::SizeHint which, const QSizeF& constraint ) const +{ + using Q = CircularChart; + + if ( which != Qt::PreferredSize ) + return QSizeF(); + + const auto sz = skinnable->strutSizeHint( CircularChart::Arc ); + + auto metrics = skinnable->arcMetricsHint( Q::Arc ); + + QSizeF hint; + + if ( metrics.isClosed() ) + { + // We only support constrained hints for closed arcs + + if ( constraint.width() >= 0.0 || constraint.height() >= 0.0 ) + { + const qreal ratio = sz.isEmpty() ? 1.0 : sz.width() / sz.height(); + + if ( constraint.width() >= 0.0 ) + { + hint = skinnable->innerBoxSize( + Q::Panel, QSizeF( constraint.width(), constraint.width() ) ); + + hint.setHeight( hint.width() / ratio ); + hint = skinnable->outerBoxSize( Q::Panel, hint ); + + return QSizeF( -1.0, hint.height() ); + } + else + { + hint = skinnable->innerBoxSize( + Q::Panel, QSizeF( constraint.height(), constraint.height() ) ); + + hint.setWidth( hint.height() * ratio ); + hint = skinnable->outerBoxSize( Q::Panel, hint ); + + return QSizeF( hint.width(), -1.0 ); + } + } + } + + if ( constraint.width() >= 0.0 || constraint.height() >= 0.0 ) + return QSizeF(); + + metrics = metrics.toAbsolute( 0.5 * sz.width(), 0.5 * sz.height() ); + + hint = metrics.boundingSize( sz ); + hint = skinnable->outerBoxSize( Q::Panel, hint ); + + return hint; +} + +#include "moc_CircularChartSkinlet.cpp" diff --git a/playground/charts/CircularChartSkinlet.h b/playground/charts/CircularChartSkinlet.h new file mode 100644 index 00000000..5bc403d6 --- /dev/null +++ b/playground/charts/CircularChartSkinlet.h @@ -0,0 +1,61 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include +#include + +class CircularChartSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole + { + PanelRole, + ArcRole, + SegmentRole, + SegmentLabelRole + }; + + Q_INVOKABLE CircularChartSkinlet( QskSkin* = nullptr ); + ~CircularChartSkinlet() override; + + void updateNode( QskSkinnable*, QSGNode* ) const override; + + QRectF subControlRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol ) const override; + + QSizeF sizeHint( const QskSkinnable*, + Qt::SizeHint, const QSizeF& ) const override; + + int sampleCount( const QskSkinnable*, QskAspect::Subcontrol ) const override; + + QRectF sampleRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol, int index ) const override; + + int sampleIndexAt( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol, const QPointF& ) const override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + QSGNode* updateSampleNode( const QskSkinnable*, + QskAspect::Subcontrol, int index, QSGNode* ) const override; + + private: + QSGNode* updateArcSegmentNode( const QskSkinnable*, QSGNode*, + qreal, const QColor&, const QskGradient&, const QskArcMetrics& ) const; + + QSGNode* updateArcLabelNode( const QskSkinnable*, QSGNode*, + const QString&, const QskArcMetrics& ) const; + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/charts/StackedChart.cpp b/playground/charts/StackedChart.cpp new file mode 100644 index 00000000..4d7bf219 --- /dev/null +++ b/playground/charts/StackedChart.cpp @@ -0,0 +1,112 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "StackedChart.h" +#include "ChartSample.h" + +#include + +class StackedChart::PrivateData +{ + public: + qreal totalValue = 0.0; + + QVector< ChartSample > samples; + QVector< qreal > cumulatedValues; +}; + +StackedChart::StackedChart( QQuickItem* parentItem ) + : QskControl( parentItem ) + , m_data( new PrivateData() ) +{ +} + +StackedChart::~StackedChart() +{ +} + +void StackedChart::setTotalValue( qreal value ) +{ + if ( value < 0.0 ) + value = 0.0; + + if ( value != m_data->totalValue ) + { + m_data->totalValue = value; + update(); + + Q_EMIT totalValueChanged( m_data->totalValue ); + } +} + +qreal StackedChart::totalValue() const +{ + return m_data->totalValue; +} + +qreal StackedChart::effectiveTotalValue() const +{ + qreal cummulated = 0.0; + + if ( !m_data->samples.isEmpty() ) + { + cummulated = m_data->cumulatedValues.last() + + m_data->samples.last().value(); + } + + return qMax( m_data->totalValue, cummulated ); +} + +QskIntervalF StackedChart::stackedInterval( int index ) const +{ + QskIntervalF interval; + + if ( index >= 0 && index < m_data->samples.size() ) + { + interval.setLowerBound( m_data->cumulatedValues[index] ); + interval.setWidth( m_data->samples[index].value() ); + } + + return interval; +} + +void StackedChart::setSeries( const QVector< ChartSample >& samples ) +{ + m_data->samples = samples; + + { + // caching the cumulated values + + m_data->cumulatedValues.reserve( samples.size() ); + + qreal total = 0.0; + for ( const auto& sample : samples ) + { + m_data->cumulatedValues += total; + total += sample.value(); + } + } + + update(); + + Q_EMIT seriesChanged(); +} + +QVector< ChartSample > StackedChart::series() const +{ + return m_data->samples; +} + +QString StackedChart::labelAt( int index ) const +{ + return m_data->samples.value( index ).title(); +} + +QskGradient StackedChart::gradientAt( int index ) const +{ + return m_data->samples.value( index ).gradient(); +} + +#include "moc_StackedChart.cpp" diff --git a/playground/charts/StackedChart.h b/playground/charts/StackedChart.h new file mode 100644 index 00000000..9efef84e --- /dev/null +++ b/playground/charts/StackedChart.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +class ChartSample; +class QskIntervalF; +class QString; + +/* + Examples for charts, that do stack their values: + + - pie charts + - donut charts + - stacked bar charts + - ... + */ +class StackedChart : public QskControl +{ + Q_OBJECT + + using Inherited = QskControl; + + Q_PROPERTY( QVector< ChartSample > series READ series + WRITE setSeries NOTIFY seriesChanged ) + + Q_PROPERTY( qreal totalValue READ totalValue + WRITE setTotalValue NOTIFY totalValueChanged ) + + public: + StackedChart( QQuickItem* parent = nullptr ); + ~StackedChart() override; + + void setTotalValue( qreal ); + qreal totalValue() const; + + QskIntervalF stackedInterval( int index ) const; + + qreal effectiveTotalValue() const; + + void setSeries( const QVector< ChartSample >& ); + QVector< ChartSample > series() const; + + virtual QskGradient gradientAt( int ) const; + virtual QString labelAt( int ) const; + + Q_SIGNALS: + void totalValueChanged( qreal ); + void seriesChanged(); + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/charts/main.cpp b/playground/charts/main.cpp new file mode 100644 index 00000000..90ab4688 --- /dev/null +++ b/playground/charts/main.cpp @@ -0,0 +1,79 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "CircularChart.h" +#include "ChartSample.h" +#include "ChartView.h" + +#include +#include +#include +#include + +#include +#include + +namespace +{ + class DistroChart : public CircularChart + { + using Inherited = CircularChart; + + public: + DistroChart( QQuickItem* parent = nullptr ) + : CircularChart( parent ) + { + setArcDiameters( 400, 300 ); + setArcThickness( 50, Qt::RelativeSize ); + + /* + DistroWatch Page Hit Ranking, Last 12 months: 2023/04/04 + https://distrowatch.com/dwres.php?resource=popularity + */ + + QVector< ChartSample > hits; + hits += { "MX Linux", 2738, QGradient::WinterNeva }; + hits += { "EndeavourOS", 2355, QGradient::StrongBliss }; + hits += { "Mint", 2062, 0xff86be43 }; + hits += { "Manjaro", 1474, QGradient::SandStrike }; + + setSeries( hits ); + +#if 1 + // only to demonstrate the effect of setTotalValue + const auto totalValue = 1.1 * effectiveTotalValue(); + setTotalValue( totalValue ); +#endif + } + + QString labelAt( int index ) const override + { + const auto value = series().value( index ).value(); + const auto ratio = value / effectiveTotalValue(); + + const QLocale l; + return l.toString( ratio * 100.0, 'f', 1 ) + l.percent(); + } + }; +} + +int main( int argc, char* argv[] ) +{ +#ifdef ITEM_STATISTICS + QskObjectCounter counter( true ); +#endif + + QGuiApplication app( argc, argv ); + + SkinnyShortcut::enable( SkinnyShortcut::AllShortcuts ); + + QskWindow window; + window.addItem( new ChartView( new DistroChart() ) ); + window.addItem( new QskFocusIndicator() ); + window.resize( 600, 500 ); + window.show(); + + return app.exec(); +}