charts code added: will become Qsk classes at some point

This commit is contained in:
Uwe Rathmann 2023-04-20 11:15:46 +02:00
parent 2d6b7b3f46
commit 207ba079a1
13 changed files with 1358 additions and 0 deletions

View File

@ -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)

View File

@ -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 )

View File

@ -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.h>
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"

View File

@ -0,0 +1,110 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskGlobal.h>
#include <QskGradient.h>
#include <qstring.h>
#include <qmetatype.h>
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

View File

@ -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 <QskArcMetrics.h>
#include <QskGraphic.h>
#include <QskRgbValue.h>
#include <QskBox.h>
#include <QskLinearBox.h>
#include <QskGridBox.h>
#include <QskTextLabel.h>
#include <QskGraphicLabel.h>
#include <QskSlider.h>
#include <qpainter.h>
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"

View File

@ -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 <QskMainView.h>
class CircularChart;
class ChartView : public QskMainView
{
public:
ChartView( CircularChart*, QQuickItem* parent = nullptr );
};

View File

@ -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 <QskArcMetrics.h>
#include <QskRgbValue.h>
#include <QskBoxBorderMetrics.h>
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"

View File

@ -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& );
};

View File

@ -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 <QskIntervalF.h>
#include <QskArcMetrics.h>
#include <QskArcNode.h>
#include <qpainterpath.h>
#include <qmath.h>
#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 <QskPaintedNode.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
#include <qbrush.h>
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"

View File

@ -0,0 +1,61 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
#include <memory>
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;
};

View File

@ -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 <QskIntervalF.h>
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"

View File

@ -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 <QskControl.h>
#include <qvector.h>
#include <qnamespace.h>
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;
};

View File

@ -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 <QskWindow.h>
#include <QskFocusIndicator.h>
#include <QskObjectCounter.h>
#include <QskRgbValue.h>
#include <SkinnyShortcut.h>
#include <QGuiApplication>
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();
}