code from features/plots merged

This commit is contained in:
Uwe Rathmann 2023-11-28 13:36:47 +01:00
parent 3c505652a3
commit 717a1c2ef2
42 changed files with 4583 additions and 394 deletions

View File

@ -6,6 +6,7 @@ add_subdirectory(invoker)
add_subdirectory(shadows)
add_subdirectory(shapes)
add_subdirectory(charts)
add_subdirectory(plots)
if (BUILD_INPUTCONTEXT)
add_subdirectory(inputpanel)

View File

@ -0,0 +1,22 @@
############################################################################
# QSkinny - Copyright (C) 2016 Uwe Rathmann
# This file may be used under the terms of the 3-clause BSD License
############################################################################
list(APPEND HEADERS QskPlotView.h QskPlotItem.h QskPlotViewSkinlet.h QskPlotNamespace.h)
list(APPEND SOURCES QskPlotView.cpp QskPlotItem.cpp QskPlotViewSkinlet.cpp)
list(APPEND HEADERS QskPlotGrid.h QskPlotGridSkinlet.h)
list(APPEND SOURCES QskPlotGrid.cpp QskPlotGridSkinlet.cpp)
list(APPEND HEADERS QskPlotCurveData.h QskPlotCurve.h QskPlotCurveSkinlet.h )
list(APPEND SOURCES QskPlotCurveData.cpp QskPlotCurve.cpp QskPlotCurveSkinlet.cpp)
list(APPEND HEADERS QskPlotCorridorData.h QskPlotCorridor.h QskPlotCorridorSkinlet.h )
list(APPEND SOURCES QskPlotCorridorData.cpp QskPlotCorridor.cpp QskPlotCorridorSkinlet.cpp)
list(APPEND HEADERS PlotCursor.h PlotCursorSkinlet.h )
list(APPEND SOURCES PlotCursor.cpp PlotCursorSkinlet.cpp)
qsk_add_example(plots ${SOURCES} ${HEADERS}
PlotSkin.h PlotSkin.cpp Plot.h Plot.cpp main.cpp )

244
playground/plots/Plot.cpp Normal file
View File

@ -0,0 +1,244 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "Plot.h"
#include "PlotSkin.h"
#include "PlotCursor.h"
#include <QskPlotGrid.h>
#include <QskPlotCorridor.h>
#include <QskPlotCorridorData.h>
#include <QskPlotCurve.h>
#include <QskPlotCurveData.h>
#include <QskEvent.h>
#include <QskMargins.h>
#include <QskRgbValue.h>
#include <QTime>
#include <cmath>
namespace
{
class CurveData : public QskPlotCurveData
{
public:
CurveData( QObject* parent = nullptr )
: QskPlotCurveData( parent )
{
setHint( MonotonicX );
}
void setSamples( const QVector< Plot::Sample >& samples )
{
m_samples = samples;
Q_EMIT changed();
}
qsizetype count() const override
{
return m_samples.count();
}
QPointF pointAt( qsizetype index ) const override
{
const auto& sample = m_samples.at( index );
return QPointF( sample.timestamp, sample.value );
}
private:
QVector< Plot::Sample > m_samples;
};
class CorridorData : public QskPlotCorridorData
{
public:
CorridorData( QObject* parent = nullptr )
: QskPlotCorridorData( parent )
{
}
void setSamples( const QVector< Plot::Sample >& samples )
{
m_samples = samples;
Q_EMIT changed();
}
qsizetype count() const override
{
return m_samples.count();
}
QskPlotCorridorSample sampleAt( qsizetype index ) const override
{
const auto& sample = m_samples.at( index );
return { sample.timestamp, { sample.lowerBound, sample.upperBound } };
}
private:
QVector< Plot::Sample > m_samples;
};
}
class Plot::PrivateData
{
public:
QskPlotGrid* grid = nullptr;
QskPlotCorridor* corridor = nullptr;
QskPlotCurve* curve = nullptr;
PlotCursor* cursor = nullptr;
const QTime startTime = QTime::currentTime();
};
Plot::Plot( QQuickItem* parentItem )
: QskPlotView( parentItem )
, m_data( new PrivateData )
{
PlotSkin::extendSkin( effectiveSkin() );
setAcceptedMouseButtons( Qt::LeftButton ); // cursor
setWheelEnabled( true ); // zooming
resetAxes();
using namespace QskRgb;
m_data->grid = new QskPlotGrid( this );
m_data->corridor = new QskPlotCorridor( this );
m_data->corridor->setData( new CorridorData() );
m_data->curve = new QskPlotCurve( this );
m_data->curve->setData( new CurveData() );
m_data->cursor = new PlotCursor();
m_data->cursor->setParent( this ); // not attached
m_data->cursor->setPosition( -33 );
/*
extra space for the overlapping labels. Actually this should be
the job of the layout code: TODO ...
*/
setPaddingHint( Panel, QskMargins( 10, 15, 40, 10 ) );
}
Plot::~Plot()
{
}
void Plot::setSamples( const QVector< Sample >& samples )
{
auto corridorData = static_cast< CorridorData* >( m_data->corridor->data() );
corridorData->setSamples( samples );
auto curveData = static_cast< CurveData* >( m_data->curve->data() );
curveData->setSamples( samples );
}
void Plot::shiftXAxis( int steps )
{
auto range = boundaries( QskPlot::XBottom );
//range.translate( steps * 0.2 * range.width() );
range.translate( steps * 1.0 );
setBoundaries( QskPlot::XBottom, range );
}
void Plot::resetAxes()
{
QskIntervalF rangeX( -50.0, 0.0 );
if ( m_data->curve && m_data->curve->data() )
{
const auto r = m_data->curve->data()->boundingRect();
if ( !r.isEmpty() )
rangeX |= QskIntervalF( r.left(), r.right() );
}
setBoundaries( QskPlot::XBottom, rangeX );
setBoundaries( QskPlot::YLeft, 0.0, 100.0 );
}
QVariant Plot::labelAt( QskPlot::Axis axis, qreal pos ) const
{
if ( axis == QskPlot::XBottom )
{
auto text = QString::number( pos, 'g' );
text += '\n';
const auto time = m_data->startTime.addSecs( qRound( pos ) );
text += time.toString();
return text;
}
return Inherited::labelAt( axis, pos );
}
void Plot::mousePressEvent( QMouseEvent* event )
{
auto pos = qskMousePosition( event );
if ( canvasRect().contains( pos ) )
{
m_data->cursor->attach( this );
m_data->cursor->setCanvasPosition( pos.x() );
return;
}
Inherited::mousePressEvent( event );
}
void Plot::mouseMoveEvent( QMouseEvent* event )
{
if ( m_data->cursor->view() )
{
auto x = qskMousePosition( event ).x();
const auto r = canvasRect();
x = qBound( r.left(), x, r.right() );
m_data->cursor->setCanvasPosition( x );
return;
}
Inherited::mouseMoveEvent( event );
}
void Plot::mouseReleaseEvent( QMouseEvent* event )
{
if ( m_data->cursor->view() )
{
m_data->cursor->detach();
return;
}
Inherited::mouseReleaseEvent( event );
}
void Plot::wheelEvent( QWheelEvent* event )
{
const auto steps = qskWheelSteps( event );
double f = std::pow( 0.9, qAbs( steps ) );
if ( steps > 0 )
f = 1 / f;
auto range = boundaries( QskPlot::XBottom );
range.setLowerBound( range.upperBound() - f * range.length() );
setBoundaries( QskPlot::XBottom, range );
}
void Plot::changeEvent( QEvent* event )
{
if ( event->type() == QEvent::StyleChange )
PlotSkin::extendSkin( effectiveSkin() );
Inherited::changeEvent( event );
}
#include "moc_Plot.cpp"

50
playground/plots/Plot.h Normal file
View File

@ -0,0 +1,50 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include "QskPlotView.h"
#include <memory>
class Plot : public QskPlotView
{
Q_OBJECT
using Inherited = QskPlotView;
public:
class Sample
{
public:
qreal timestamp = 0.0;
qreal lowerBound = 0.0;
qreal value = 0.0;
qreal upperBound = 0.0;
};
Plot( QQuickItem* parentItem = nullptr );
~Plot() override;
void setSamples( const QVector< Sample >& );
public Q_SLOT:
void resetAxes();
void shiftXAxis( int steps );
protected:
void mousePressEvent( QMouseEvent* ) override;
void mouseMoveEvent( QMouseEvent* ) override;
void mouseReleaseEvent( QMouseEvent* ) override;
void wheelEvent( QWheelEvent* ) override;
void changeEvent( QEvent* ) override;
private:
QVariant labelAt( QskPlot::Axis axis, qreal pos ) const final override;
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,81 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "PlotCursor.h"
#include <qtransform.h>
QSK_SUBCONTROL( PlotCursor, Line )
QSK_SUBCONTROL( PlotCursor, LabelPanel )
QSK_SUBCONTROL( PlotCursor, LabelText )
class PlotCursor::PrivateData
{
public:
qreal position = 0.0;
Qt::Orientation orientation = Qt::Horizontal;
};
PlotCursor::PlotCursor( QObject* object )
: Inherited( object )
, m_data( new PrivateData )
{
setCoordinateType( CanvasCoordinates );
}
PlotCursor::~PlotCursor()
{
}
void PlotCursor::setOrientation( Qt::Orientation orientation )
{
if ( m_data->orientation != orientation )
{
m_data->orientation = orientation;
markDirty();
}
}
Qt::Orientation PlotCursor::orientation() const
{
return m_data->orientation;
}
void PlotCursor::setCanvasPosition( qreal position )
{
const auto t = transformation().inverted();
if ( m_data->orientation == Qt::Horizontal )
position = t.map( QPointF( position, 0.0 ) ).x();
else
position = t.map( QPointF( 0.0, position ) ).y();
setPosition( position );
}
void PlotCursor::setPosition( qreal position )
{
if ( m_data->position != position )
{
m_data->position = position;
markDirty();
}
}
qreal PlotCursor::position() const
{
return m_data->position;
}
void PlotCursor::transformationChanged( ChangeFlags flags )
{
Inherited::transformationChanged( flags );
}
bool PlotCursor::needsClipping() const
{
return false;
}
#include "moc_PlotCursor.cpp"

View File

@ -0,0 +1,37 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include "QskPlotItem.h"
#include <qnamespace.h>
class PlotCursor : public QskPlotItem
{
Q_OBJECT
using Inherited = QskPlotItem;
public:
QSK_SUBCONTROLS( Line, LabelPanel, LabelText )
PlotCursor( QObject* = nullptr );
~PlotCursor() override;
void setOrientation( Qt::Orientation );
Qt::Orientation orientation() const;
void setCanvasPosition( qreal );
void setPosition( qreal );
qreal position() const;
void transformationChanged( ChangeFlags ) override;
bool needsClipping() const override;
private:
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,211 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "PlotCursorSkinlet.h"
#include "PlotCursor.h"
#include <QskSGNode.h>
#include <QskTextOptions.h>
#include <QskTextColors.h>
#include <QskPlotCurve.h>
#include <QskPlotCorridor.h>
#include <QskPlotCorridorData.h>
#include <QskPlotView.h>
#include <QskFunctions.h>
#include <qtransform.h>
#include <qfontmetrics.h>
#include <qcolor.h>
enum { Lower, Upper, Value };
static inline QskAspect::Variation variation( int index )
{
if ( index == Lower )
return QskAspect::Lower;
if ( index == Upper )
return QskAspect::Upper;
return QskAspect::NoVariation;
}
class PlotCursorSkinlet::PrivateData
{
public:
struct
{
qreal value;
} labelInfo[ 3 ];
};
PlotCursorSkinlet::PlotCursorSkinlet( QskSkin* skin )
: Inherited( skin )
, m_data( new PrivateData )
{
setNodeRoles( { CursorLine, TextBox, Text } );
}
PlotCursorSkinlet::~PlotCursorSkinlet()
{
}
void PlotCursorSkinlet::updateNode(
QskSkinnable* skinnable, QSGNode* parent ) const
{
const auto cursor = static_cast< const PlotCursor* >( skinnable );
const auto x = cursor->position();
auto info = m_data->labelInfo;
for ( auto child : cursor->view()->children() )
{
if ( auto curve = qobject_cast< const QskPlotCurve* >( child ) )
{
const auto pos = curve->interpolatedPoint( Qt::Horizontal, x );
info[Value].value = pos.y();
}
else if ( auto corridor = qobject_cast< const QskPlotCorridor* >( child ) )
{
const auto boundary = corridor->interpolatedSample( x ).boundary;
info[ Lower ].value = boundary.lowerBound();
info[ Upper ].value = boundary.upperBound();
}
}
Inherited::updateNode( skinnable, parent );
}
QSGNode* PlotCursorSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
using Q = PlotCursor;
switch( nodeRole )
{
case CursorLine:
return updateCursorLineNode( skinnable, node );
case TextBox:
return updateSeriesNode( skinnable, Q::LabelPanel, node );
case Text:
return updateSeriesNode( skinnable, Q::LabelText, node );
}
return Inherited::updateSubNode( skinnable, nodeRole, node );
}
int PlotCursorSkinlet::sampleCount(
const QskSkinnable*, QskAspect::Subcontrol ) const
{
return 3;
}
QRectF PlotCursorSkinlet::sampleRect( const QskSkinnable* skinnable,
const QRectF&, QskAspect::Subcontrol, int index ) const
{
using Q = PlotCursor;
const auto cursor = static_cast< const PlotCursor* >( skinnable );
auto pos = QPointF( cursor->position(),
m_data->labelInfo[ index ].value );
pos = cursor->transformation().map( pos );
const QFontMetricsF fm( cursor->effectiveFont( Q::LabelText ) );
const qreal w = qskHorizontalAdvance( fm, "100.0" );
const qreal h = fm.height();
QRectF r( 0, 0, w, h );
r = r.marginsAdded( cursor->paddingHint( Q::LabelPanel ) );
r.moveRight( pos.x() - 5 );
r.moveBottom( pos.y() );
return r;
}
QSGNode* PlotCursorSkinlet::updateSampleNode( const QskSkinnable* skinnable,
QskAspect::Subcontrol subControl, int index, QSGNode* node ) const
{
using Q = PlotCursor;
const auto rect = sampleRect( skinnable, QRectF(), subControl, index );
const auto aspect = subControl | variation( index );
if ( subControl == Q::LabelPanel )
{
const auto gradient = skinnable->gradientHint( aspect );
return updateBoxNode( skinnable, node, rect, gradient, subControl );
}
if ( subControl == Q::LabelText )
{
const auto info = m_data->labelInfo[ index ];
const auto text = QString::number( info.value, 'f', 1 );
const auto color = skinnable->color( aspect );
const auto textOptions = skinnable->textOptionsHint( aspect );
const auto font = skinnable->effectiveFont( aspect );
return updateTextNode( skinnable, node, rect, Qt::AlignCenter,
text, font, textOptions, color, Qsk::Normal );
}
return nullptr;
}
QSGNode* PlotCursorSkinlet::updateCursorLineNode(
const QskSkinnable* skinnable, QSGNode* node ) const
{
auto cursor = static_cast< const PlotCursor* >( skinnable );
const auto r = cursor->scaleRect();
if ( r.isEmpty() )
return nullptr;
QPointF p1 = r.topLeft();
QPointF p2 = r.bottomRight();
if ( cursor->orientation() == Qt::Horizontal )
{
const auto x = cursor->position();
if ( x < r.left() || x > r.right() )
return nullptr;
p1.rx() = p2.rx() = x;
}
else
{
const auto y = cursor->position();
if ( y < r.top() || y > r.bottom() )
return nullptr;
p1.ry() = p2.ry() = y;
}
if ( cursor->coordinateType() == QskPlotItem::CanvasCoordinates )
{
/*
When having a non solid line we might want to use CanvasCoordinates
to avoid the length of dashes/dots being affected from the
plot transformation.
*/
const auto transform = cursor->transformation();
p1 = transform.map( p1 );
p2 = transform.map( p2 );
}
return updateLineNode( cursor, node, QLineF( p1, p2 ), PlotCursor::Line );
}
#include "moc_PlotCursorSkinlet.cpp"

View File

@ -0,0 +1,42 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
class PlotCursorSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole { CursorLine, TextBox, Text };
Q_INVOKABLE PlotCursorSkinlet( QskSkin* = nullptr );
~PlotCursorSkinlet() override;
void updateNode( QskSkinnable*, QSGNode* ) const override;
int sampleCount( const QskSkinnable*,
QskAspect::Subcontrol ) const override final;
QRectF sampleRect( const QskSkinnable*,
const QRectF&, QskAspect::Subcontrol, int index ) 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* updateCursorLineNode( const QskSkinnable*, QSGNode* ) const;
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,160 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "PlotSkin.h"
#include <QskSkin.h>
#include <QskSkinHintTableEditor.h>
#include <QskPlotView.h>
#include <QskPlotViewSkinlet.h>
#include <QskPlotCurve.h>
#include <QskPlotCurveSkinlet.h>
#include <QskPlotGrid.h>
#include <QskPlotGridSkinlet.h>
#include <QskPlotCorridor.h>
#include <QskPlotCorridorSkinlet.h>
#include "PlotCursor.h"
#include "PlotCursorSkinlet.h"
#include <QskRgbValue.h>
#include <QskBoxBorderColors.h>
#include <QskBoxShapeMetrics.h>
#include <QskStippleMetrics.h>
#include <QskScaleRenderer.h>
#include <qbrush.h>
namespace
{
inline bool isExtended( const QskSkin* skin )
{
auto metaObject = skin->skinletMetaObject( &QskPlotView::staticMetaObject );
return metaObject != &QskSkinlet::staticMetaObject; // the fallback for controls
}
class SkinEditor : private QskSkinHintTableEditor
{
public:
SkinEditor( QskSkinHintTable* table );
void setupPlotHints();
};
}
void PlotSkin::extendSkin( QskSkin* skin )
{
if ( skin == nullptr || isExtended( skin ) )
return;
skin->declareSkinlet< QskPlotView, QskPlotViewSkinlet >();
skin->declareSkinlet< QskPlotGrid, QskPlotGridSkinlet >();
skin->declareSkinlet< QskPlotCurve, QskPlotCurveSkinlet >();
skin->declareSkinlet< QskPlotCorridor, QskPlotCorridorSkinlet >();
skin->declareSkinlet< PlotCursor, PlotCursorSkinlet >();
SkinEditor editor( &skin->hintTable() );
editor.setupPlotHints();
}
SkinEditor::SkinEditor( QskSkinHintTable* table )
: QskSkinHintTableEditor( table )
{
}
void SkinEditor::setupPlotHints()
{
using A = QskAspect;
using namespace QskRgb;
const auto rgbLower = DodgerBlue;
const auto rgbUpper = MediumSeaGreen;
const auto rgbValue = Yellow;
{
using Q = QskPlotView;
// Panel
setBoxShape( Q::Panel, 10 );
setGradient( Q::Panel, qRgb( 240, 240, 240 ) );
setBoxBorderMetrics( Q::Panel, 1 );
setBoxBorderColors( Q::Panel, qRgb( 220, 220, 220 ) );
// Canvas
setGradient( Q::Canvas, QGradient::PremiumDark );
setBoxBorderColors( Q::Canvas, DimGray );
setBoxBorderMetrics( Q::Canvas, 2 );
setBoxShape( Q::Canvas, 4 );
// AxisScale
const auto padding = 4; // spacing between canvas and axis
setPadding( Q::AxisScale | A::Left, 0, 0, padding, 0 );
setPadding( Q::AxisScale | A::Bottom, 0, padding, 0, 0 );
setColor( Q::AxisScale, qRgb( 20, 20, 20 ) );
setFontRole( Q::AxisScale, QskSkin::MediumFont );
setFlag( Q::AxisScale | A::Style, QskScaleRenderer::Backbone );
// thickness/length of the major ticks
setStrutSize( Q::AxisScale, 1.0, 8.0 );
// spacing between ticks and labels
setSpacing( Q::AxisScale, 5 );
}
{
using Q = QskPlotGrid;
setColor( Q::MajorLine, White );
setMetric( Q::MajorLine | A::Size, 1 );
setStippleMetrics( Q::MajorLine, { 2, 6 } );
setColor( Q::MinorLine, Gainsboro );
setMetric( Q::MinorLine | A::Size, 1 );
setStippleMetrics( Q::MinorLine, { 1, 10 } );
}
{
using Q = QskPlotCurve;
setMetric( Q::Line | A::Size, 2 );
setColor( Q::Line, rgbValue );
}
{
using Q = QskPlotCorridor;
setMetric( Q::Border | A::Size, 2 );
setColor( Q::Border | A::Lower, rgbLower );
setColor( Q::Border | A::Upper, rgbUpper );
setColor( Q::Corridor, toTransparent( Crimson, 150 ) );
}
{
using Q = PlotCursor;
const int alpha = 200;
setColor( Q::Line, Qt::yellow );
setMetric( Q::Line | A::Size, 1 );
setStippleMetrics( Q::Line, { 4, 8 } );
setGradient( Q::LabelPanel | A::Lower, toTransparent( rgbLower, alpha ) );
setColor( Q::LabelText | A::Lower, Qt::white );
setGradient( Q::LabelPanel | A::Upper, toTransparent( rgbUpper, alpha ) );
setColor( Q::LabelText | A::Upper, Qt::white );
setGradient( Q::LabelPanel, toTransparent( rgbValue, alpha ) );
setColor( Q::LabelText, Qt::black );
setBoxShape( Q::LabelPanel, 5 );
setPadding( Q::LabelPanel, 5, 5, 5, 5 );
}
}

View File

@ -0,0 +1,13 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
class QskSkin;
namespace PlotSkin
{
void extendSkin( QskSkin* );
};

View File

@ -0,0 +1,156 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotCorridor.h"
#include "QskPlotCorridorData.h"
#include <QskIntervalF.h>
#include <qcolor.h>
#include <qpointer.h>
QSK_SUBCONTROL( QskPlotCorridor, Border )
QSK_SUBCONTROL( QskPlotCorridor, Corridor )
class QskPlotCorridor::PrivateData
{
public:
QPointer< QskPlotCorridorData > corridorData;
};
QskPlotCorridor::QskPlotCorridor( QObject* object )
: Inherited( object )
, m_data( new PrivateData )
{
}
QskPlotCorridor::~QskPlotCorridor()
{
}
void QskPlotCorridor::setBorderWidth( qreal lineWidth )
{
const auto aspect = Border | QskAspect::Size;
resetMetric( aspect | QskAspect::Lower );
resetMetric( aspect | QskAspect::Upper );
lineWidth = qMax( lineWidth, 0.0 );
if ( setMetric( aspect, lineWidth ) )
{
markDirty();
Q_EMIT borderWidthChanged( lineWidth );
}
}
qreal QskPlotCorridor::borderWidth() const
{
return metric( Border | QskAspect::Size );
}
void QskPlotCorridor::setBorderColor( const QColor& color )
{
resetColor( Border | QskAspect::Lower );
resetColor( Border | QskAspect::Upper );
if ( setColor( Border, color ) )
{
markDirty();
Q_EMIT colorChanged( color );
}
}
QColor QskPlotCorridor::borderColor() const
{
return color( Border );
}
void QskPlotCorridor::setColor( const QColor& color )
{
if ( setColor( Corridor, color ) )
{
markDirty();
Q_EMIT colorChanged( color );
}
}
QColor QskPlotCorridor::color() const
{
return color( Corridor );
}
void QskPlotCorridor::setSamples( const QVector< QskPlotCorridorSample >& samples )
{
setData( new QskPlotCorridorSamples( samples, this ) );
}
void QskPlotCorridor::setData( QskPlotCorridorData* corridorData )
{
if ( corridorData == m_data->corridorData )
return;
auto oldData = m_data->corridorData.data();
m_data->corridorData = corridorData;
if ( oldData )
{
if ( oldData->parent() == this )
{
delete oldData;
}
else
{
disconnect( oldData, &QskPlotCorridorData::changed,
this, &QskPlotItem::markDirty );
}
}
if ( corridorData )
{
if ( corridorData->parent() == nullptr )
corridorData->setParent( this );
connect( corridorData, &QskPlotCorridorData::changed,
this, &QskPlotItem::markDirty );
}
markDirty();
}
QskPlotCorridorData* QskPlotCorridor::data() const
{
return m_data->corridorData;
}
QskPlotCorridorSample QskPlotCorridor::interpolatedSample( qreal value ) const
{
if ( m_data->corridorData )
return m_data->corridorData->interpolatedSample( value );
return QskPlotCorridorSample();
}
void QskPlotCorridor::transformationChanged( ChangeFlags flags )
{
Inherited::transformationChanged( flags );
}
bool QskPlotCorridor::needsClipping() const
{
auto data = m_data->corridorData.data();
if ( data == nullptr || data->count() == 0 )
return false;
// The skinlet does basic polygon clipping in x direction
const auto corridorRect = data->boundingRect();
const auto plotRect = scaleRect();
return ( corridorRect.top() < plotRect.top() )
|| ( corridorRect.bottom() > plotRect.bottom() );
}
#include "moc_QskPlotCorridor.cpp"

View File

@ -0,0 +1,67 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include "QskPlotItem.h"
#include <qvector.h>
class QskPlotCorridorSample;
class QskPlotCorridorData;
class QColor;
// Only horizontal: TODO
class QskPlotCorridor : public QskPlotItem
{
Q_OBJECT
using Inherited = QskPlotItem;
Q_PROPERTY( qreal borderWidth READ borderWidth
WRITE setBorderWidth NOTIFY borderWidthChanged )
Q_PROPERTY( QColor borderColor READ borderColor
WRITE setBorderColor NOTIFY borderColorChanged )
Q_PROPERTY( QColor color READ color
WRITE setColor NOTIFY colorChanged )
public:
QSK_SUBCONTROLS( Border, Corridor )
QskPlotCorridor( QObject* = nullptr );
~QskPlotCorridor() override;
void setBorderWidth( qreal );
qreal borderWidth() const;
void setBorderColor( const QColor& );
QColor borderColor() const;
void setColor( const QColor& );
QColor color() const;
void setSamples( const QVector< QskPlotCorridorSample >& );
void setData( QskPlotCorridorData* );
QskPlotCorridorData* data() const;
QskPlotCorridorSample interpolatedSample( qreal value ) const;
void transformationChanged( ChangeFlags ) override;
bool needsClipping() const override;
using QskSkinnable::setColor;
using QskSkinnable::color;
Q_SIGNALS:
void borderWidthChanged( qreal );
void borderColorChanged( const QColor& );
void colorChanged( const QColor& );
private:
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,138 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotCorridorData.h"
namespace
{
inline int upperIndex( const QskPlotCorridorData* data, qreal value )
{
const int indexMax = data->count() - 1;
if ( indexMax < 0 || data->sampleAt( indexMax ).value < value )
return -1;
int indexMin = 0;
int n = indexMax;
while ( n > 0 )
{
const int half = n >> 1;
const int indexMid = indexMin + half;
if ( value < data->sampleAt( indexMid ).value )
{
n = half;
}
else
{
indexMin = indexMid + 1;
n -= half + 1;
}
}
return indexMin;
}
QRectF boundingRect( const QskPlotCorridorData* data )
{
const auto count = data->count();
if ( count <= 0 )
return QRectF();
auto boundary = data->sampleAt( 0 ).boundary;
for ( int i = 1; i < count; i++ )
boundary.unite( data->sampleAt( i ).boundary );
const auto x1 = data->sampleAt( 0 ).value;
const auto x2 = data->sampleAt( count - 1 ).value;
return QRectF( x1, boundary.lowerBound(),
x2 - x1, boundary.length() ).normalized();
}
}
QskPlotCorridorData::QskPlotCorridorData( QObject* parent )
: QObject( parent )
{
}
QskPlotCorridorData::~QskPlotCorridorData()
{
}
QRectF QskPlotCorridorData::boundingRect() const
{
if ( m_boundingRect.isNull() )
m_boundingRect = ::boundingRect( this );
return m_boundingRect;
}
int QskPlotCorridorData::upperIndex( qreal value ) const
{
const int n = count();
if ( n == 0 )
return -1;
auto index = ::upperIndex( this, value );
if ( ( index == -1 ) && ( value == sampleAt( n - 1 ).value ) )
index = n - 1;
return index;
}
QskPlotCorridorSample QskPlotCorridorData::interpolatedSample( qreal value ) const
{
const int n = count();
if ( n == 0 )
return QskPlotCorridorSample();
if ( n == 1 )
return { value, { 0.0, 0.0 } };
int index = 0;
if ( n > 2 )
{
index = upperIndex( value );
if ( index > 0 )
index--;
}
const auto s1 = sampleAt( index );
const auto s2 = sampleAt( index + 1 );
const auto dv = s2.value - s1.value;
if ( dv == 0.0 )
return s2;
const auto t = ( value - s1.value ) / dv;
return { value, s1.boundary.interpolated( s2.boundary, t ) };
}
QskPlotCorridorSamples::QskPlotCorridorSamples( QObject* parent )
: QskPlotCorridorData( parent )
{
}
QskPlotCorridorSamples::QskPlotCorridorSamples(
const QVector< QskPlotCorridorSample >& samples, QObject* parent )
: QskPlotCorridorData( parent )
, m_samples( samples )
{
}
void QskPlotCorridorSamples::setSamples( const QVector< QskPlotCorridorSample >& samples )
{
m_samples = samples;
m_boundingRect = QRectF(); // invalidating
Q_EMIT changed();
}
#include "moc_QskPlotCorridorData.cpp"

View File

@ -0,0 +1,82 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include <QskIntervalF.h>
#include <qobject.h>
#include <qvector.h>
#include <qrect.h>
class QskPlotCorridorSample
{
public:
qreal value = 0.0;
QskIntervalF boundary;
};
Q_DECLARE_TYPEINFO( QskPlotCorridorSample, Q_MOVABLE_TYPE );
// Hiding the layout of the data behind an abstract API
class QskPlotCorridorData : public QObject
{
Q_OBJECT
public:
QskPlotCorridorData( QObject* parent = nullptr );
virtual ~QskPlotCorridorData();
virtual qsizetype count() const = 0;
virtual QskPlotCorridorSample sampleAt( qsizetype index ) const = 0;
virtual QRectF boundingRect() const;
int upperIndex( qreal value ) const;
QskPlotCorridorSample interpolatedSample( qreal value ) const;
Q_SIGNALS:
void changed();
protected:
mutable QRectF m_boundingRect;
};
// A simple implementation using QVector< CorridorSample >
class QskPlotCorridorSamples : public QskPlotCorridorData
{
Q_OBJECT
using Inherited = QskPlotCorridorData;
public:
QskPlotCorridorSamples( QObject* parent = nullptr );
QskPlotCorridorSamples(
const QVector< QskPlotCorridorSample >&, QObject* parent = nullptr );
void setSamples( const QVector< QskPlotCorridorSample >& );
QVector< QskPlotCorridorSample > samples() const;
qsizetype count() const override;
QskPlotCorridorSample sampleAt( qsizetype index ) const override;
private:
QVector< QskPlotCorridorSample > m_samples;
};
inline QVector< QskPlotCorridorSample > QskPlotCorridorSamples::samples() const
{
return m_samples;
}
inline qsizetype QskPlotCorridorSamples::count() const
{
return m_samples.count();
}
inline QskPlotCorridorSample QskPlotCorridorSamples::sampleAt( qsizetype index ) const
{
return m_samples.at( index );
}

View File

@ -0,0 +1,261 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskPlotCorridorSkinlet.h"
#include "QskPlotCorridor.h"
#include "QskPlotCorridorData.h"
#include <QskSGNode.h>
#include <QskVertex.h>
#include <qvector.h>
#include <qsgvertexcolormaterial.h>
namespace
{
class GeometryNode : public QSGGeometryNode
{
protected:
GeometryNode()
: m_geometry( QSGGeometry::defaultAttributes_ColoredPoint2D(), 0 )
{
setGeometry( &m_geometry );
setMaterial( &m_material );
}
private:
QSGGeometry m_geometry;
QSGVertexColorMaterial m_material;
};
class CorridorNode : public GeometryNode
{
public:
void updateCorridor( const QskPlotCorridorData* data,
const QskPlotCorridorSample& sample1, int index1,
const QskPlotCorridorSample& sample2, int index2,
const QColor& color )
{
using namespace QskVertex;
const Color vertexColor( color );
geometry()->setDrawingMode( QSGGeometry::DrawTriangleStrip );
auto line = allocateLines< ColoredLine >( *geometry(), index2 - index1 + 1 );
line++->setLine( sample1.value, sample1.boundary.lowerBound(),
sample1.value, sample1.boundary.upperBound(), vertexColor );
for ( int i = index1 + 1; i < index2; i++ )
{
const auto sample = data->sampleAt( i );
line++->setLine( sample.value, sample.boundary.lowerBound(),
sample.value, sample.boundary.upperBound(), vertexColor );
}
line++->setLine( sample2.value, sample2.boundary.lowerBound(),
sample2.value, sample2.boundary.upperBound(), vertexColor );
markDirty( QSGNode::DirtyGeometry );
}
};
class BorderNode : public GeometryNode
{
public:
void updateBorder( const QskPlotCorridorData* data,
const QskPlotCorridorSample& sample1, int index1,
const QskPlotCorridorSample& sample2, int index2,
quint8 nodeRole, const QColor& color, qreal lineWidth )
{
auto& geometry = *this->geometry();
geometry.setDrawingMode( QSGGeometry::DrawLineStrip );
const float lineWidthF = lineWidth;
if( lineWidthF != geometry.lineWidth() )
geometry.setLineWidth( lineWidthF );
const QskVertex::Color c( color );
geometry.allocate( index2 - index1 + 1 );
auto p = geometry.vertexDataAsColoredPoint2D();
if( nodeRole == QskPlotCorridorSkinlet::LowerBoundRole )
{
p++->set( sample1.value, sample1.boundary.lowerBound(),
c.r, c.g, c.b, c.a );
for ( int i = index1 + 1; i < index2; i++ )
{
const auto sample = data->sampleAt( i );
p++->set( sample.value, sample.boundary.lowerBound(),
c.r, c.g, c.b, c.a );
}
p++->set( sample2.value, sample2.boundary.lowerBound(),
c.r, c.g, c.b, c.a );
}
else
{
p++->set( sample1.value, sample1.boundary.upperBound(),
c.r, c.g, c.b, c.a );
for ( int i = index1 + 1; i < index2; i++ )
{
const auto sample = data->sampleAt( i );
p++->set( sample.value, sample.boundary.upperBound(),
c.r, c.g, c.b, c.a );
}
p++->set( sample2.value, sample2.boundary.upperBound(),
c.r, c.g, c.b, c.a );
}
markDirty( QSGNode::DirtyGeometry );
}
};
}
class QskPlotCorridorSkinlet::PrivateData
{
public:
int index1, index2;
QskPlotCorridorSample sample1, sample2;
};
QskPlotCorridorSkinlet::QskPlotCorridorSkinlet( QskSkin* skin )
: Inherited( skin )
, m_data( new PrivateData )
{
setNodeRoles( { CorridorRole, LowerBoundRole, UpperBoundRole } );
}
QskPlotCorridorSkinlet::~QskPlotCorridorSkinlet()
{
}
void QskPlotCorridorSkinlet::updateNode(
QskSkinnable* skinnable, QSGNode* parent ) const
{
/*
As clipping is the same for borders and corridor
we do it only once here
*/
auto corridor = static_cast< const QskPlotCorridor* >( skinnable );
const auto data = corridor->data();
m_data->index1 = m_data->index2 = -1;
const auto n = data->count();
if ( data && n > 0 )
{
auto& s1 = m_data->sample1;
auto& s2 = m_data->sample2;
s1 = data->sampleAt( 0 );
s2 = data->sampleAt( n - 1 );
const auto scaleRect = corridor->scaleRect();
const qreal x1 = scaleRect.left();
const qreal x2 = scaleRect.right();
if ( !( x1 > s2.value || x2 < s1.value ) )
{
m_data->index1 = 0;
m_data->index2 = n - 1;
const int index1 = data->upperIndex( x1 );
if ( index1 > 0 )
{
m_data->index1 = index1 - 1;
s1 = data->interpolatedSample( x1 );
}
const int index2 = data->upperIndex( x2 );
if ( index2 > 0 )
{
m_data->index2 = index2;
s2 = data->interpolatedSample( x2 );
}
}
}
Inherited::updateNode( skinnable, parent );
}
QSGNode* QskPlotCorridorSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
if ( m_data->index2 < 0 )
return nullptr;
if ( nodeRole == CorridorRole )
return updateCorridorNode( skinnable, node );
else
return updateBorderNode( skinnable, nodeRole, node );
}
QSGNode* QskPlotCorridorSkinlet::updateCorridorNode(
const QskSkinnable* skinnable, QSGNode* node ) const
{
using Q = QskPlotCorridor;
auto corridor = static_cast< const QskPlotCorridor* >( skinnable );
const auto color = corridor->color( Q::Corridor );
if ( !color.isValid() || color.alpha() == 0 )
return nullptr;
const auto corridorData = corridor->data();
if ( corridorData->count() == 0 )
return nullptr;
auto corridorNode = QskSGNode::ensureNode< CorridorNode >( node );
corridorNode->updateCorridor( corridorData,
m_data->sample1, m_data->index1, m_data->sample2, m_data->index2, color );
return corridorNode;
}
QSGNode* QskPlotCorridorSkinlet::updateBorderNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
using Q = QskPlotCorridor;
using A = QskAspect;
auto corridor = static_cast< const QskPlotCorridor* >( skinnable );
const auto corridorData = corridor->data();
if ( corridorData->count() == 0 )
return nullptr;
QColor color;
if( nodeRole == QskPlotCorridorSkinlet::LowerBoundRole )
color = corridor->color( Q::Border | A::Lower );
else
color = corridor->color( Q::Border | A::Upper );
if ( !color.isValid() || color.alpha() == 0 )
return nullptr;
auto lineWidth = corridor->metric( Q::Border | A::Size );
lineWidth = qMax( lineWidth, 0.0 );
auto borderNode = QskSGNode::ensureNode< BorderNode >( node );
borderNode->updateBorder( corridorData,
m_data->sample1, m_data->index1, m_data->sample2, m_data->index2,
nodeRole, color, lineWidth );
return borderNode;
}
#include "moc_QskPlotCorridorSkinlet.cpp"

View File

@ -0,0 +1,39 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
class QskPlotCorridorSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole
{
CorridorRole,
LowerBoundRole,
UpperBoundRole
};
Q_INVOKABLE QskPlotCorridorSkinlet( QskSkin* = nullptr );
~QskPlotCorridorSkinlet() override;
void updateNode( QskSkinnable*, QSGNode* ) const override;
protected:
QSGNode* updateSubNode( const QskSkinnable*,
quint8 nodeRole, QSGNode* ) const override;
private:
QSGNode* updateCorridorNode( const QskSkinnable*, QSGNode* ) const;
QSGNode* updateBorderNode( const QskSkinnable*, quint8, QSGNode* ) const;
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,156 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotCurve.h"
#include "QskPlotCurveData.h"
#include <qpointer.h>
#include <qcolor.h>
QSK_SUBCONTROL( QskPlotCurve, Line )
class QskPlotCurve::PrivateData
{
public:
QPointer< QskPlotCurveData > curveData;
};
QskPlotCurve::QskPlotCurve( QObject* object )
: Inherited( object )
, m_data( new PrivateData )
{
}
QskPlotCurve::~QskPlotCurve()
{
}
void QskPlotCurve::setColor( const QColor& color )
{
if ( setColor( Line, color ) )
{
markDirty();
Q_EMIT colorChanged( color );
}
}
QColor QskPlotCurve::color() const
{
return color( Line );
}
void QskPlotCurve::setLineWidth( qreal lineWidth )
{
lineWidth = qMax( lineWidth, 0.0 );
if ( setMetric( Line | QskAspect::Size, lineWidth ) )
{
markDirty();
Q_EMIT lineWidthChanged( lineWidth );
}
}
qreal QskPlotCurve::lineWidth() const
{
return metric( Line | QskAspect::Size );
}
void QskPlotCurve::setPoints( const QVector< QPointF >& points )
{
setData( new QskPlotCurvePoints( points, this ) );
}
void QskPlotCurve::setData( QskPlotCurveData* curveData )
{
if ( curveData == m_data->curveData )
return;
auto oldData = m_data->curveData.data();
m_data->curveData = curveData;
if ( oldData )
{
if ( oldData->parent() == this )
delete oldData;
else
disconnect( oldData, &QskPlotCurveData::changed, this, &QskPlotItem::markDirty );
}
if ( curveData )
{
if ( curveData->parent() == nullptr )
curveData->setParent( this );
connect( curveData, &QskPlotCurveData::changed, this, &QskPlotItem::markDirty );
}
markDirty();
}
QskPlotCurveData* QskPlotCurve::data() const
{
return m_data->curveData;
}
QPointF QskPlotCurve::interpolatedPoint(
Qt::Orientation orientation, qreal value ) const
{
if ( m_data->curveData )
return m_data->curveData->interpolatedPoint( orientation, value );
return QPointF();
}
void QskPlotCurve::transformationChanged( ChangeFlags flags )
{
if ( flags & ( XBoundariesChanged | YBoundariesChanged ) )
{
/*
We could skip updates, when the curve is inside
of old and new boundaries TODO ...
*/
}
Inherited::transformationChanged( flags );
}
bool QskPlotCurve::needsClipping() const
{
auto data = m_data->curveData.data();
if ( data == nullptr || data->count() == 0 )
return false;
// The skinlet does basic polygon clipping for monotonic data.
using D = QskPlotCurveData;
const auto hints = data->hints();
if ( ( hints & D::MonotonicX ) && ( hints & D::MonotonicY ) )
return false;
if ( data->hints() & QskPlotCurveData::BoundingRectangle )
{
const auto curveRect = data->boundingRect();
const auto plotRect = scaleRect();
if ( hints & D::MonotonicX )
{
return ( curveRect.top() < plotRect.top() )
|| ( curveRect.bottom() > plotRect.bottom() );
}
if ( hints & D::MonotonicY )
{
return ( curveRect.left() < plotRect.left() )
&& ( curveRect.right() > plotRect.right() );
}
return !plotRect.contains( curveRect );
}
return true;
}
#include "moc_QskPlotCurve.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 "QskPlotItem.h"
#include <qpoint.h>
#include <qvector.h>
class QskPlotCurveData;
class QColor;
class QskPlotCurve : public QskPlotItem
{
Q_OBJECT
Q_PROPERTY( qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged )
Q_PROPERTY( QColor color READ color WRITE setColor NOTIFY colorChanged )
using Inherited = QskPlotItem;
public:
QSK_SUBCONTROLS( Line )
QskPlotCurve( QObject* = nullptr );
~QskPlotCurve() override;
void setPoints( const QVector< QPointF >& );
void setData( QskPlotCurveData* );
QskPlotCurveData* data() const;
QPointF interpolatedPoint( Qt::Orientation, qreal ) const;
void setColor( const QColor& );
QColor color() const;
void setLineWidth( qreal );
qreal lineWidth() const;
void transformationChanged( ChangeFlags ) override;
bool needsClipping() const override;
using QskSkinnable::setColor;
using QskSkinnable::color;
Q_SIGNALS:
void lineWidthChanged( qreal );
void colorChanged( const QColor& );
private:
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,285 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotCurveData.h"
#include <qdebug.h>
#include <algorithm>
#include <cstddef>
namespace
{
struct compareX
{
inline bool operator()( const double x, const QPointF& pos ) const
{
return ( x < pos.x() );
}
};
struct compareY
{
inline bool operator()( const double y, const QPointF& pos ) const
{
return ( y < pos.y() );
}
};
template< typename LessThan >
inline int upperIndex(
const QskPlotCurveData* data, qreal value, LessThan lessThan )
{
const int indexMax = data->count() - 1;
if ( indexMax < 0 || !lessThan( value, data->pointAt( indexMax ) ) )
return -1;
int indexMin = 0;
int n = indexMax;
while ( n > 0 )
{
const int half = n >> 1;
const int indexMid = indexMin + half;
if ( lessThan( value, data->pointAt( indexMid ) ) )
{
n = half;
}
else
{
indexMin = indexMid + 1;
n -= half + 1;
}
}
return indexMin;
}
}
namespace
{
QRectF boundingRect( const QskPlotCurveData* data )
{
const auto count = data->count();
if ( count <= 0 )
return QRectF();
const auto hints = data->hints();
const bool montonicX = hints & QskPlotCurveData::MonotonicX;
const bool montonicY = hints & QskPlotCurveData::MonotonicY;
if ( montonicX && montonicY )
{
const auto p1 = data->pointAt( 0 );
const auto p2 = data->pointAt( count - 1 );
return QRectF( p1, p2 ).normalized();
}
if ( montonicX )
{
const auto p1 = data->pointAt( 0 );
const auto p2 = data->pointAt( count - 1 );
qreal yMin, yMax;
yMin = yMax = p1.y();
for ( int i = 1; i < count; i++ )
{
const auto p = data->pointAt( i );
if ( p.y() < yMin )
yMin = p.y();
else if ( p.y() > yMax )
yMax = p.y();
}
return QRectF( p1.x(), yMin, p2.x() - p1.x(), yMax - yMin ).normalized();
}
if ( montonicY )
{
const auto p1 = data->pointAt( 0 );
const auto p2 = data->pointAt( count - 1 );
qreal xMin, xMax;
xMin = xMax = p1.x();
for ( int i = 1; i < count; i++ )
{
const auto p = data->pointAt( i );
if ( p.x() < xMin )
xMin = p.x();
else if ( p.x() > xMax )
xMax = p.x();
}
return QRectF( xMin, p1.y(), xMax - xMin, p2.y() - p1.y() ).normalized();
}
{
const auto p1 = data->pointAt( 0 );
qreal xMin, xMax;
qreal yMin, yMax;
xMin = xMax = p1.x();
yMin = yMax = p1.y();
for ( int i = 1; i < count; i++ )
{
const auto p = data->pointAt( i );
if ( p.x() < xMin )
xMin = p.x();
else if ( p.x() > xMax )
xMax = p.x();
if ( p.y() < yMin )
yMin = p.y();
else if ( p.y() > yMax )
yMax = p.y();
}
return QRectF( xMin, yMin, xMax - xMin, yMax - yMin );
}
}
}
QskPlotCurveData::QskPlotCurveData( QObject* parent )
: QObject( parent )
{
}
QskPlotCurveData::~QskPlotCurveData()
{
}
void QskPlotCurveData::setHints( Hints hints )
{
if ( m_hints != hints )
{
m_hints = hints;
Q_EMIT changed();
}
}
void QskPlotCurveData::setHint( Hint hint, bool on )
{
if ( on )
setHints( m_hints | hint );
else
setHints( m_hints & ~hint );
}
QRectF QskPlotCurveData::boundingRect() const
{
if ( m_boundingRect.isNull() )
m_boundingRect = ::boundingRect( this );
return m_boundingRect;
}
int QskPlotCurveData::upperIndex( Qt::Orientation orientation, qreal value ) const
{
const int n = count();
if ( n == 0 )
return -1;
int index;
if ( orientation == Qt::Horizontal )
{
index = ::upperIndex( this, value, compareX() );
if ( ( index == -1 ) && ( value == pointAt( n - 1 ).x() ) )
index = n - 1;
}
else
{
index = ::upperIndex( this, value, compareY() );
if ( ( index == -1 ) && ( value == pointAt( n - 1 ).y() ) )
index = n - 1;
}
return index;
}
QPointF QskPlotCurveData::interpolatedPoint(
Qt::Orientation orientation, qreal value ) const
{
const int n = count();
if ( n == 0 )
return QPointF();
if ( n == 1 )
{
if ( orientation == Qt::Horizontal )
return QPointF( value, 0.0 );
else
return QPointF( 0.0, value );
}
int index = 0;
if ( n > 2 )
{
index = upperIndex( orientation, value );
if ( index > 0 )
index--;
}
const auto p1 = pointAt( index );
const auto p2 = pointAt( index + 1 );
if ( orientation == Qt::Horizontal )
{
const auto dx = p2.x() - p1.x();
if ( dx == 0.0 )
return p2;
const auto t = ( value - p1.x() ) / dx;
const auto y = p1.y() + t * ( p2.y() - p1.y() );
return QPointF( value, y );
}
else
{
const auto dy = p2.y() - p1.y();
if ( dy == 0.0 )
return p2;
const auto t = ( value - p1.y() ) / dy;
const auto x = p1.x() + t * ( p2.x() - p1.x() );
return QPointF( x, value );
}
}
QskPlotCurvePoints::QskPlotCurvePoints( QObject* parent )
: QskPlotCurveData( parent )
{
}
QskPlotCurvePoints::QskPlotCurvePoints(
const QVector< QPointF >& points, QObject* parent )
: QskPlotCurveData( parent )
, m_points( points )
{
}
void QskPlotCurvePoints::setPoints( const QVector< QPointF >& points )
{
m_points = points;
m_boundingRect = QRectF(); // invalidating
Q_EMIT changed();
}
#include "moc_QskPlotCurveData.cpp"

View File

@ -0,0 +1,105 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include <qobject.h>
#include <qvector.h>
#include <qrect.h>
#include <qnamespace.h>
// Hiding the layout of the data behind an abstract API
class QskPlotCurveData : public QObject
{
Q_OBJECT
public:
enum Hint
{
/*
The points are monotonic in/decreasing. F.e a faster algos
can be implemented with this information ( f.e polygon clipping )
*/
MonotonicX = 1 << 0,
MonotonicY = 1 << 1,
/*
The data offers a bounding rectangle, that can f.e be used for
clipping or autoscaling purposes
*/
BoundingRectangle = 1 << 2
};
Q_ENUM( Hint );
Q_DECLARE_FLAGS( Hints, Hint )
QskPlotCurveData( QObject* parent = nullptr );
virtual ~QskPlotCurveData();
void setHints( Hints );
Hints hints() const;
void setHint( Hint, bool on = true );
virtual qsizetype count() const = 0;
virtual QPointF pointAt( qsizetype index ) const = 0;
virtual QRectF boundingRect() const;
int upperIndex( Qt::Orientation, qreal value ) const;
QPointF interpolatedPoint( Qt::Orientation, qreal value ) const;
Q_SIGNALS:
void changed();
protected:
mutable QRectF m_boundingRect;
private:
Hints m_hints = BoundingRectangle;
};
inline QskPlotCurveData::Hints QskPlotCurveData::hints() const
{
return m_hints;
}
Q_DECLARE_OPERATORS_FOR_FLAGS( QskPlotCurveData::Hints )
// A simple implementation using QVector< QPointF >
class QskPlotCurvePoints : public QskPlotCurveData
{
Q_OBJECT
using Inherited = QskPlotCurveData;
public:
QskPlotCurvePoints( QObject* parent = nullptr );
QskPlotCurvePoints( const QVector< QPointF >&, QObject* parent = nullptr );
void setPoints( const QVector< QPointF >& );
QVector< QPointF > points() const;
qsizetype count() const override;
QPointF pointAt( qsizetype index ) const override;
private:
QVector< QPointF > m_points;
};
inline QVector< QPointF > QskPlotCurvePoints::points() const
{
return m_points;
}
inline qsizetype QskPlotCurvePoints::count() const
{
return m_points.count();
}
inline QPointF QskPlotCurvePoints::pointAt( qsizetype index ) const
{
return m_points.at( index );
}

View File

@ -0,0 +1,163 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskPlotCurveSkinlet.h"
#include "QskPlotCurveData.h"
#include "QskPlotCurve.h"
#include <QskSGNode.h>
#include <QskVertex.h>
#include <qsggeometry.h>
#include <qsgvertexcolormaterial.h>
namespace
{
class CurveNode : public QSGGeometryNode
{
public:
CurveNode()
: m_geometry( QSGGeometry::defaultAttributes_ColoredPoint2D(), 0 )
{
setGeometry( &m_geometry );
setMaterial( &m_material );
}
void updateCurve( const QRectF& scaleRect, const QskPlotCurveData* data,
const QColor& color, qreal lineWidth )
{
m_geometry.setDrawingMode( QSGGeometry::DrawLineStrip );
const float lineWidthF = lineWidth;
if( lineWidthF != m_geometry.lineWidth() )
m_geometry.setLineWidth( lineWidthF );
const QskVertex::Color c( color );
int from = 0;
int to = data->count() - 1;
auto point1 = data->pointAt( from );
auto point2 = data->pointAt( to );
if ( data->hints() & QskPlotCurveData::MonotonicX )
{
const qreal x1 = scaleRect.left();
const qreal x2 = scaleRect.right();
if ( x1 > point2.x() || x2 < point1.x() )
{
QskSGNode::resetGeometry( this );
return;
}
const int index1 = data->upperIndex( Qt::Horizontal, x1 );
if ( index1 > 0 )
{
from = index1 - 1;
point1 = data->interpolatedPoint( Qt::Horizontal, x1 );
}
const int index2 = data->upperIndex( Qt::Horizontal, x2 );
if ( index2 > 0 )
{
to = index2;
point2 = data->interpolatedPoint( Qt::Horizontal, x2 );
}
}
else if ( data->hints() & QskPlotCurveData::MonotonicY )
{
const qreal y1 = scaleRect.top();
const qreal y2 = scaleRect.bottom();
if ( y1 > point2.y() || y2 < point1.y() )
{
QskSGNode::resetGeometry( this );
return;
}
const int index1 = data->upperIndex( Qt::Vertical, y1 );
if ( index1 > 0 )
{
from = index1 - 1;
point1 = data->interpolatedPoint( Qt::Vertical, y1 );
}
const int index2 = data->upperIndex( Qt::Vertical, y2 );
if ( index2 > 0 )
{
to = index2;
point2 = data->interpolatedPoint( Qt::Vertical, y2 );
}
}
m_geometry.allocate( to - from + 1 );
auto p = m_geometry.vertexDataAsColoredPoint2D();
p++->set( point1.x(), point1.y(), c.r, c.g, c.b, c.a );
for ( int i = from + 1; i < to; i++ )
{
const auto point = data->pointAt( i );
p++->set( point.x(), point.y(), c.r, c.g, c.b, c.a );
}
p++->set( point2.x(), point2.y(), c.r, c.g, c.b, c.a );
markDirty( QSGNode::DirtyGeometry );
}
private:
QSGGeometry m_geometry;
QSGVertexColorMaterial m_material;
};
}
QskPlotCurveSkinlet::QskPlotCurveSkinlet( QskSkin* skin )
: Inherited( skin )
{
setNodeRoles( { Polygon } );
}
QskPlotCurveSkinlet::~QskPlotCurveSkinlet()
{
}
QSGNode* QskPlotCurveSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
if ( nodeRole == Polygon )
return updatePolygonNode( skinnable, node );
return Inherited::updateSubNode( skinnable, nodeRole, node );
}
QSGNode* QskPlotCurveSkinlet::updatePolygonNode(
const QskSkinnable* skinnable, QSGNode* node ) const
{
using Q = QskPlotCurve;
auto curve = static_cast< const QskPlotCurve* >( skinnable );
const auto curveData = curve->data();
if ( curveData == nullptr || curveData->count() == 0 )
return nullptr;
const auto color = curve->color( Q::Line );
if ( !color.isValid() || color.alpha() == 0 )
return nullptr;
const auto lineWidth = curve->metric( Q::Line | QskAspect::Size );
if ( lineWidth <= 0.0 )
return nullptr;
auto curveNode = QskSGNode::ensureNode< CurveNode >( node );
curveNode->updateCurve( curve->scaleRect(), curveData, color, lineWidth );
return curveNode;
}
#include "moc_QskPlotCurveSkinlet.cpp"

View File

@ -0,0 +1,28 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
class QskPlotCurveSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole { Polygon };
Q_INVOKABLE QskPlotCurveSkinlet( QskSkin* = nullptr );
~QskPlotCurveSkinlet() override;
protected:
QSGNode* updateSubNode( const QskSkinnable*,
quint8 nodeRole, QSGNode* ) const override;
private:
QSGNode* updatePolygonNode( const QskSkinnable*, QSGNode* ) const;
};

View File

@ -0,0 +1,137 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotGrid.h"
#include "QskPlotView.h"
#include "QskStippleMetrics.h"
#include <QskTickmarks.h>
QSK_SUBCONTROL( QskPlotGrid, MajorLine )
QSK_SUBCONTROL( QskPlotGrid, MinorLine )
static inline QskAspect::Subcontrol qskSubcontrol( QskPlotGrid::Type gridType )
{
using Q = QskPlotGrid;
return ( gridType == Q::MinorGrid ) ? Q::MinorLine : Q::MajorLine;
}
class QskPlotGrid::PrivateData
{
};
QskPlotGrid::QskPlotGrid( QObject* object )
: Inherited( object )
, m_data( new PrivateData )
{
setCoordinateType( CanvasCoordinates );
}
QskPlotGrid::~QskPlotGrid()
{
}
void QskPlotGrid::setPen( Type gridType, const QPen& pen )
{
using A = QskAspect;
const auto oldPen = this->pen( gridType );
const auto subControl = qskSubcontrol( gridType );
setColor( subControl, pen.color() );
setMetric( subControl | A::Size, pen.widthF() );
setSkinHint( subControl | A::Metric | A::Style,
QVariant::fromValue( QskStippleMetrics( pen ) ) );
if ( oldPen != pen )
{
markDirty();
if ( gridType == MinorGrid )
Q_EMIT minorPenChanged( pen );
else
Q_EMIT majorPenChanged( pen );
}
}
void QskPlotGrid::resetPen( Type gridType )
{
using A = QskAspect;
const auto oldPen = pen( gridType );
const auto subControl = qskSubcontrol( gridType );
resetColor( subControl );
resetMetric( subControl | A::Size );
resetMetric( subControl | A::Style );
const auto newPen = pen( gridType );
if ( oldPen != newPen )
{
markDirty();
if ( gridType == MinorGrid )
Q_EMIT minorPenChanged( newPen );
else
Q_EMIT majorPenChanged( newPen );
}
}
QPen QskPlotGrid::pen( Type gridType ) const
{
using A = QskAspect;
const auto subControl = qskSubcontrol( gridType );
const auto stippleMetrics = effectiveSkinHint(
subControl | A::Metric | A::Style ).value< QskStippleMetrics >();
QPen pen( Qt::NoPen );
if ( stippleMetrics.isValid() )
{
if ( stippleMetrics.isSolid() )
{
pen.setStyle( Qt::SolidLine );
}
else
{
pen.setStyle( Qt::CustomDashLine );
pen.setDashOffset( stippleMetrics.offset() );
pen.setDashPattern( stippleMetrics.pattern() );
}
pen.setColor( color( subControl ) );
pen.setWidth( metric( subControl | A::Size ) );
}
return pen;
}
QVector< qreal > QskPlotGrid::lines( Type gridType, Qt::Orientation orientation ) const
{
if ( auto view = this->view() )
{
const auto axis = ( orientation == Qt::Horizontal ) ? yAxis() : xAxis();
const auto& tickmarks = view->tickmarks( axis );
if ( gridType == MajorGrid )
return tickmarks.majorTicks();
else
return tickmarks.mediumTicks() + tickmarks.minorTicks();
}
return QVector< qreal >();
}
bool QskPlotGrid::needsClipping() const
{
return false;
}
#include "moc_QskPlotGrid.cpp"

View File

@ -0,0 +1,97 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include "QskPlotItem.h"
#include <qnamespace.h>
#include <qvector.h>
#include <qpen.h>
class QPen;
class QskPlotGrid : public QskPlotItem
{
Q_OBJECT
Q_PROPERTY( QPen minorPen READ minorPen
WRITE setMinorPen RESET resetMinorPen NOTIFY minorPenChanged )
Q_PROPERTY( QPen majorPen READ majorPen
WRITE setMajorPen RESET resetMajorPen NOTIFY majorPenChanged )
using Inherited = QskPlotItem;
public:
QSK_SUBCONTROLS( MajorLine, MinorLine )
enum Type
{
MinorGrid,
MajorGrid
};
Q_ENUM( Type )
QskPlotGrid( QObject* = nullptr );
~QskPlotGrid() override;
void setPen( Type, const QPen& );
void resetPen( Type );
QPen pen( Type ) const;
void setMajorPen( const QPen& );
void resetMajorPen();
QPen majorPen() const;
void setMinorPen( const QPen& );
void resetMinorPen();
QPen minorPen() const;
// positions
virtual QVector< qreal > lines( Type, Qt::Orientation ) const;
bool needsClipping() const override;
Q_SIGNALS:
void minorPenChanged( const QPen& );
void majorPenChanged( const QPen& );
private:
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};
inline void QskPlotGrid::setMinorPen( const QPen& pen )
{
setPen( MinorGrid, pen );
}
inline void QskPlotGrid::resetMinorPen()
{
resetPen( MinorGrid );
}
inline QPen QskPlotGrid::minorPen() const
{
return pen( MinorGrid );
}
inline void QskPlotGrid::setMajorPen( const QPen& pen )
{
setPen( MajorGrid, pen );
}
inline void QskPlotGrid::resetMajorPen()
{
resetPen( MajorGrid );
}
inline QPen QskPlotGrid::majorPen() const
{
return pen( MajorGrid );
}

View File

@ -0,0 +1,81 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskPlotGridSkinlet.h"
#include "QskPlotGrid.h"
#include "QskStippleMetrics.h"
#include <QskSGNode.h>
#include <QskIntervalF.h>
#include <QskLinesNode.h>
#include <qpen.h>
QskPlotGridSkinlet::QskPlotGridSkinlet( QskSkin* skin )
: Inherited( skin )
{
setNodeRoles( { MinorGrid, MajorGrid } );
}
QskPlotGridSkinlet::~QskPlotGridSkinlet()
{
}
QSGNode* QskPlotGridSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
using Q = QskPlotGrid;
if ( nodeRole == MinorGrid )
return updateGridNode( skinnable, Q::MinorLine, node );
if ( nodeRole == MajorGrid )
return updateGridNode( skinnable, Q::MajorLine, node );
return Inherited::updateSubNode( skinnable, nodeRole, node );
}
QSGNode* QskPlotGridSkinlet::updateGridNode( const QskSkinnable* skinnable,
QskAspect::Subcontrol subControl, QSGNode* node ) const
{
using Q = QskPlotGrid;
const auto gridType =
( subControl == Q::MinorLine ) ? Q::MinorGrid : Q::MajorGrid;
auto grid = static_cast< const QskPlotGrid* >( skinnable );
const auto r = grid->scaleRect();
if ( r.isEmpty() )
return nullptr;
const auto stipple = grid->stippleMetricsHint( subControl );
if ( !stipple.isValid() )
return nullptr;
const auto lineColor = grid->color( subControl );
if ( !lineColor.isValid() || lineColor.alpha() == 0 )
return nullptr;
const auto lineWidth = grid->metric( subControl | QskAspect::Size );
if ( lineWidth <= 0 )
return nullptr;
const auto xValues = grid->lines( gridType, Qt::Vertical );
const auto yValues = grid->lines( gridType, Qt::Horizontal );
if ( xValues.isEmpty() && yValues.isEmpty() )
return nullptr;
auto gridNode = QskSGNode::ensureNode< QskLinesNode >( node );
gridNode->setPixelAlignment( Qt::Horizontal | Qt::Vertical );
gridNode->updateGrid( lineColor, lineWidth,
stipple, grid->transformation(), r, xValues, yValues );
return gridNode;
}
#include "moc_QskPlotGridSkinlet.cpp"

View File

@ -0,0 +1,29 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
class QskPlotGridSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole { MinorGrid, MajorGrid };
Q_INVOKABLE QskPlotGridSkinlet( QskSkin* = nullptr );
~QskPlotGridSkinlet() override;
protected:
QSGNode* updateSubNode( const QskSkinnable*,
quint8 nodeRole, QSGNode* ) const override;
private:
QSGNode* updateGridNode(
const QskSkinnable*, QskAspect::Subcontrol, QSGNode* ) const;
};

View File

@ -0,0 +1,201 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotItem.h"
#include "QskPlotView.h"
#include <QskSkinlet.h>
#include <QskIntervalF.h>
#include <qpointer.h>
class QskPlotItem::PrivateData
{
public:
QskPlot::Axis xAxis = QskPlot::XBottom;
QskPlot::Axis yAxis = QskPlot::YLeft;
qreal z = 0.0;
QPointer< QskPlotView > view;
CoordinateType coordinateType = PlotCoordinates;
bool dirty = true;
};
QskPlotItem::QskPlotItem( QObject* parent )
: QObject( parent )
, m_data( new PrivateData )
{
if ( auto view = qobject_cast< QskPlotView* >( parent ) )
attach( view );
}
QskPlotItem::~QskPlotItem()
{
attach( nullptr );
}
void QskPlotItem::attach( QskPlotView* view )
{
if ( view == m_data->view )
return;
if ( m_data->view )
m_data->view->detachItem( this );
m_data->view = view;
if ( m_data->view )
m_data->view->attachItem( this );
}
void QskPlotItem::setAxes( QskPlot::Axis xAxis, QskPlot::Axis yAxis )
{
setXAxis( xAxis );
setYAxis( yAxis );
}
QskPlot::Axis QskPlotItem::xAxis() const
{
return m_data->xAxis;
}
void QskPlotItem::setXAxis( QskPlot::Axis axis )
{
if ( m_data->xAxis != axis )
{
m_data->xAxis = axis;
Q_EMIT axisChanged();
markDirty();
}
}
QskPlot::Axis QskPlotItem::yAxis() const
{
return m_data->yAxis;
}
void QskPlotItem::setYAxis( QskPlot::Axis axis )
{
if ( m_data->yAxis != axis )
{
m_data->yAxis = axis;
Q_EMIT axisChanged();
markDirty();
}
}
qreal QskPlotItem::z() const
{
return m_data->z;
}
bool QskPlotItem::isDirty() const
{
return m_data->dirty;
}
void QskPlotItem::setZ( qreal z )
{
if ( m_data->z != z )
{
m_data->z = z;
Q_EMIT zChanged( z );
if ( auto view = m_data->view )
{
view->detachItem( this );
view->attachItem( this );
}
}
}
void QskPlotItem::markDirty()
{
if ( !m_data->dirty )
{
m_data->dirty = true;
updatePlot();
}
}
void QskPlotItem::resetDirty()
{
m_data->dirty = false;
}
void QskPlotItem::updatePlot()
{
if ( m_data->view )
m_data->view->update();
}
void QskPlotItem::setCoordinateType( CoordinateType type )
{
if ( m_data->coordinateType != type )
{
m_data->coordinateType = type;
m_data->dirty = true;
}
}
QskPlotItem::CoordinateType QskPlotItem::coordinateType() const
{
return m_data->coordinateType;
}
QTransform QskPlotItem::transformation() const
{
if ( m_data->view )
return m_data->view->transformation( m_data->xAxis, m_data->yAxis );
return QTransform();
}
const QskPlotView* QskPlotItem::view() const
{
return m_data->view;
}
QQuickItem* QskPlotItem::owningItem() const
{
return m_data->view;
}
void QskPlotItem::updateNode( QSGNode* node )
{
if ( auto skinlet = effectiveSkinlet() )
skinlet->updateNode( this, node );
resetDirty();
}
bool QskPlotItem::needsClipping() const
{
return false;
}
void QskPlotItem::transformationChanged( ChangeFlags flags )
{
if ( flags == CanvasGeometryChanged && coordinateType() == PlotCoordinates )
return;
markDirty();
}
QRectF QskPlotItem::scaleRect() const
{
if ( auto view = m_data->view )
{
return QskIntervalF::toRect(
view->boundaries( m_data->xAxis ),
view->boundaries( m_data->yAxis ) );
}
return QRectF();
}
#include "moc_QskPlotItem.cpp"

View File

@ -0,0 +1,122 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#pragma once
#include "QskPlotNamespace.h"
#include <QskSkinnable.h>
#include <qobject.h>
class QskPlotView;
class QskIntervalF;
class QskPlotItem : public QObject, public QskSkinnable
{
Q_OBJECT
Q_PROPERTY( QskPlot::Axis xAxis READ xAxis WRITE setXAxis NOTIFY axisChanged )
Q_PROPERTY( QskPlot::Axis yAxis READ yAxis WRITE setYAxis NOTIFY axisChanged )
Q_PROPERTY( qreal z READ z WRITE setZ NOTIFY zChanged )
public:
/*
The item can decide if it wants to use plot ( scales ) coordinates or
canvas ( QQuickItem ) coordinates for the vertexes of its scenegraph nodes.
PlotItems, that represent some sort of data ( f.e curves ) usually
prefer to use plot coordinates, while decorations ( f.e legend ) are
often aligned to the plot canvas geometry.
Items in plot coordinates often do not need to be updated when
the geometry of the plot canvas or the scales have been changed.
To opt out from these updates the plot item needs to overload
the transformationChange() hook.
*/
enum CoordinateType
{
CanvasCoordinates,
PlotCoordinates
};
Q_ENUM( CoordinateType );
enum ChangeFlag
{
XBoundariesChanged = 1 << 0,
XTickmarksChanged = 1 << 1,
YBoundariesChanged = 1 << 1,
YTickmarksChanged = 1 << 2,
CanvasGeometryChanged = 1 << 2
};
Q_ENUM( ChangeFlag );
Q_DECLARE_FLAGS( ChangeFlags, ChangeFlag );
QskPlotItem( QObject* = nullptr );
~QskPlotItem() override;
void attach( QskPlotView* );
void detach();
void setXAxis( QskPlot::Axis );
QskPlot::Axis xAxis() const;
void setYAxis( QskPlot::Axis );
QskPlot::Axis yAxis() const;
void setAxes( QskPlot::Axis xAxis, QskPlot::Axis yAxis );
void setZ( qreal );
qreal z() const;
void setCoordinateType( CoordinateType );
CoordinateType coordinateType() const;
/*
Indicates if the item depends on clipping
Batching of node updates is one of the main performance features
of the scene graph - however clipping breaks batching.
A plot that has no plot item, that needs clipping, can decide
to skip inserting a clip node.
*/
virtual bool needsClipping() const;
QTransform transformation() const;
QskIntervalF boundaries( Qt::Orientation ) const;
QRectF scaleRect() const;
const QskPlotView* view() const;
void markDirty();
void resetDirty();
bool isDirty() const;
virtual void updateNode( QSGNode* );
virtual void transformationChanged( ChangeFlags );
Q_SIGNALS:
void axisChanged();
void zChanged( qreal );
private:
QQuickItem* owningItem() const override final;
void updatePlot();
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};
Q_DECLARE_OPERATORS_FOR_FLAGS( QskPlotItem::ChangeFlags )
inline void QskPlotItem::detach()
{
attach( nullptr );
}

View File

@ -0,0 +1,21 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <qmetaobject.h>
namespace QskPlot
{
Q_NAMESPACE
// for the moment only 2 axes
enum Axis
{
XBottom = 0,
YLeft = 1
};
Q_ENUM_NS( Axis )
}

View File

@ -0,0 +1,371 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "QskPlotView.h"
#include "QskPlotItem.h"
#include <QskTickmarks.h>
#include <QskGraduation.h>
#include <QskSkinlet.h>
#include <QskQuick.h>
#include <QskBoxClipNode.h>
#include <QskBoxBorderMetrics.h>
#include <qsgnode.h>
#include <vector>
QSK_SUBCONTROL( QskPlotView, Panel )
QSK_SUBCONTROL( QskPlotView, AxisScale )
QSK_SUBCONTROL( QskPlotView, Canvas )
enum { AxisCount = 2 };
static inline bool qskIsXAxis( int axis )
{
return axis == QskPlot::XBottom;
}
static inline QTransform qskScaleTransform( Qt::Orientation orientation,
qreal s1, qreal s2, qreal p1, qreal p2 )
{
const auto ratio = ( p2 - p1 ) / ( s2 - s1 );
if ( orientation == Qt::Horizontal )
{
auto transform = QTransform::fromTranslate( -s1, 0.0 );
transform *= QTransform::fromScale( ratio, 1.0 );
transform *= QTransform::fromTranslate( p1, 0.0 );
return transform;
}
else
{
auto transform = QTransform::fromTranslate( 0.0, -s1 );
transform *= QTransform::fromScale( 1.0, -ratio );
transform *= QTransform::fromTranslate( 0.0, p2 );
return transform;
}
}
class QskPlotView::PrivateData
{
public:
struct
{
QskIntervalF boundaries;
QskTickmarks tickmarks;
bool boundariesDirty = true;
bool ticksDirty = true;
} scales[ AxisCount ];
class ItemData
{
public:
QskPlotItem* plotItem = nullptr;
QSGTransformNode* node = nullptr;
};
inline bool needsClipping() const
{
for ( const auto& data : itemData )
{
if ( data.plotItem->needsClipping() )
return true;
}
return false;
}
std::vector< ItemData > itemData; // a flat map
QVector< QSGNode* > orphanedNodes;
QRectF canvasRect;
};
QskPlotView::QskPlotView( QQuickItem* parentItem )
: Inherited( parentItem )
, m_data( new PrivateData )
{
setBoundaries( QskPlot::XBottom, 0.0, 100.0 );
setBoundaries( QskPlot::YLeft, 0.0, 100.0 );
}
QskPlotView::~QskPlotView()
{
}
void QskPlotView::setBoundaries( QskPlot::Axis axis, qreal from, qreal to )
{
setBoundaries( axis, QskIntervalF( from, to ) );
}
void QskPlotView::setBoundaries( QskPlot::Axis axis, const QskIntervalF& boundaries )
{
auto& sd = m_data->scales[ axis ];
if ( boundaries == sd.boundaries )
return;
sd.boundaries = boundaries;
sd.boundariesDirty = true;
const auto oldTickmarks = sd.tickmarks;
sd.tickmarks = QskGraduation::divideInterval(
boundaries.lowerBound(), boundaries.upperBound(), 5, 5 );
if ( oldTickmarks != sd.tickmarks )
sd.ticksDirty = true;
polish();
update();
}
QskIntervalF QskPlotView::boundaries( QskPlot::Axis axis ) const
{
return m_data->scales[ axis ].boundaries;
}
QskTickmarks QskPlotView::tickmarks( QskPlot::Axis axis ) const
{
return m_data->scales[ axis ].tickmarks;
}
QTransform QskPlotView::transformation( QskPlot::Axis xAxis, QskPlot::Axis yAxis ) const
{
const auto r = canvasRect();
const auto scales = m_data->scales;
const qreal x1 = scales[xAxis].boundaries.lowerBound();
const qreal x2 = scales[xAxis].boundaries.upperBound();
const qreal y1 = scales[yAxis].boundaries.lowerBound();
const qreal y2 = scales[yAxis].boundaries.upperBound();
auto transform = QTransform::fromTranslate( -x1, -y1 );
transform *= QTransform::fromScale(
r.width() / ( x2 - x1 ), r.height() / ( y1 - y2 ) );
transform *= QTransform::fromTranslate( r.left(), r.bottom() );
return transform;
}
QRectF QskPlotView::canvasRect() const
{
const auto r = subControlRect( Canvas );
const auto b = boxBorderMetricsHint( Canvas );
return r.adjusted( b.left(), b.top(), -b.right(), -b.bottom() );
}
QVariant QskPlotView::labelAt( QskPlot::Axis, qreal pos ) const
{
return QString::number( pos, 'g' );
}
void QskPlotView::changeEvent( QEvent* event )
{
if ( event->type() == QEvent::StyleChange )
{
for ( auto& itemData : m_data->itemData )
{
auto plotItem = itemData.plotItem;
if ( plotItem->skinlet() == nullptr )
plotItem->setSkinlet( nullptr ); // clearing the cached skinlet
plotItem->markDirty();
}
polish();
}
}
void QskPlotView::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry )
{
Inherited::geometryChange( newGeometry, oldGeometry );
polish();
}
void QskPlotView::attachItem( QskPlotItem* plotItem )
{
const auto cmp = []( const qreal& z, const PrivateData::ItemData& data )
{ return z < data.plotItem->z(); };
auto& itemData = m_data->itemData;
auto it = std::upper_bound( itemData.begin(), itemData.end(), plotItem->z(), cmp );
itemData.insert( it, { plotItem, nullptr } );
plotItem->markDirty();
update();
}
void QskPlotView::detachItem( QskPlotItem* plotItem )
{
auto& itemData = m_data->itemData;
for ( auto it = itemData.begin(); it != itemData.end(); ++it )
{
if ( it->plotItem == plotItem )
{
if ( it->node )
m_data->orphanedNodes += it->node;
itemData.erase( it );
update();
return;
}
}
}
void QskPlotView::updateResources()
{
using namespace QskPlot;
using I = QskPlotItem;
/*
updateResources is called from updatePolish.
We should find a better name: TODO ...
*/
I::ChangeFlags flags[ AxisCount ];
{
bool canvasChanged = false;
{
const auto canvasRect = this->canvasRect();
if ( canvasRect != m_data->canvasRect )
{
m_data->canvasRect = canvasRect;
canvasChanged = true;
}
}
for ( int axis = 0; axis < AxisCount; axis++ )
{
I::ChangeFlags flag;
auto& scaleData = m_data->scales[ axis ];
if ( canvasChanged )
flag |= I::CanvasGeometryChanged;
if ( scaleData.boundariesDirty )
flag |= qskIsXAxis( axis ) ? I::XBoundariesChanged : I::YBoundariesChanged;
if ( scaleData.ticksDirty )
flag |= qskIsXAxis( axis ) ? I::XTickmarksChanged : I::YTickmarksChanged;
flags[axis] = flag;
scaleData.boundariesDirty = scaleData.ticksDirty = false;
}
}
if ( auto itemFlags = ( flags[0] | flags[1] ) )
{
for ( auto& itemData : m_data->itemData )
itemData.plotItem->transformationChanged( itemFlags );
}
}
void QskPlotView::updateNode( QSGNode* node )
{
if ( !m_data->orphanedNodes.empty() )
{
for ( auto node : m_data->orphanedNodes )
{
if ( auto parentNode = node->parent() )
parentNode->removeChildNode( node );
delete node;
}
m_data->orphanedNodes.clear();
}
// sorting items according to z TODO ...
auto oldItemsNode = node->lastChild();
if ( oldItemsNode )
node->removeChildNode( oldItemsNode );
/*
the scene graph subtree might have been removed for situations
like skin changes and the item node pointers are dangling pointers
*/
const bool danglingNodes = ( oldItemsNode == nullptr );
Inherited::updateNode( node );
auto itemsNode = oldItemsNode;
if ( m_data->needsClipping() )
{
if ( itemsNode == nullptr || itemsNode->type() != QSGNode::ClipNodeType )
itemsNode = new QskBoxClipNode();
}
else
{
if ( itemsNode == nullptr || itemsNode->type() == QSGNode::ClipNodeType )
itemsNode = new QSGNode();
}
if ( oldItemsNode && itemsNode != oldItemsNode )
{
oldItemsNode->reparentChildNodesTo( itemsNode );
delete oldItemsNode;
}
node->appendChildNode( itemsNode );
if ( itemsNode->type() == QSGNode::ClipNodeType )
{
/*
updating the clip node:
By setting the clipNode above the geometry nodes of the plot items
we only need on clip node for all plot items. However we exclude all
nodes from scene graph batching. It might be better that each plot
item has its own clip node. But we could share their clipping vertexes.
TODO ...
*/
itemsNode = QskSkinlet::updateBoxClipNode( this,
itemsNode, subControlRect( Canvas ), Canvas );
}
for ( auto& itemData : m_data->itemData )
{
if ( danglingNodes || itemData.node == nullptr )
{
itemData.node = new QSGTransformNode();
itemsNode->appendChildNode( itemData.node );
}
QMatrix4x4 matrix;
if ( itemData.plotItem->coordinateType() == QskPlotItem::PlotCoordinates )
matrix = itemData.plotItem->transformation();
if ( matrix != itemData.node->matrix() )
itemData.node->setMatrix( matrix );
if ( itemData.plotItem->isDirty() )
itemData.plotItem->updateNode( itemData.node );
}
}
#include "moc_QskPlotView.cpp"

View File

@ -0,0 +1,56 @@
/******************************************************************************
* 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 "QskPlotNamespace.h"
class QskPlotItem;
class QskTickmarks;
class QskIntervalF;
class QTransform;
class QskPlotView : public QskControl
{
Q_OBJECT
using Inherited = QskControl;
public:
QSK_SUBCONTROLS( Panel, AxisScale, Canvas )
QskPlotView( QQuickItem* parent = nullptr );
~QskPlotView() override;
void setBoundaries( QskPlot::Axis, qreal, qreal );
void setBoundaries( QskPlot::Axis, const QskIntervalF& );
QskIntervalF boundaries( QskPlot::Axis axis ) const;
QskTickmarks tickmarks( QskPlot::Axis axis ) const;
// scales -> item coordinates
QTransform transformation( QskPlot::Axis xAxis, QskPlot::Axis yAxis ) const;
QRectF canvasRect() const;
virtual QVariant labelAt( QskPlot::Axis, qreal pos ) const;
protected:
void geometryChange( const QRectF&, const QRectF& ) override;
void updateNode( QSGNode* ) override;
void updateResources() override;
void changeEvent( QEvent* ) override;
private:
friend class QskPlotItem;
void attachItem( QskPlotItem* );
void detachItem( QskPlotItem* );
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};

View File

@ -0,0 +1,283 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskPlotViewSkinlet.h"
#include "QskPlotView.h"
#include <QskScaleRenderer.h>
#include <QskTickmarks.h>
#include <QskTextColors.h>
#include <QskFunctions.h>
#include <QskSGNode.h>
#include <QskBoxBorderMetrics.h>
#include <QskMargins.h>
#include <QskSkin.h>
#include <qfontmetrics.h>
#include <qquickwindow.h>
static inline QskTextColors qskTextColors(
const QskSkinnable* skinnable, QskAspect aspect )
{
using A = QskAspect;
QskSkinHintStatus status;
QskTextColors c;
c.textColor = skinnable->color( aspect | A::TextColor, &status );
if ( status.aspect.subControl() != aspect.subControl() )
{
// using the same color as the one for the ticks
c.textColor = skinnable->color( aspect );
}
c.styleColor = skinnable->color( aspect | A::StyleColor );
c.linkColor = skinnable->color( aspect | A::LinkColor );
return c;
}
static inline QskAspect qskAxisAspect( QskPlot::Axis axis )
{
using Q = QskPlotView;
using A = QskAspect;
return Q::AxisScale | ( ( axis == QskPlot::XBottom ) ? A::Bottom : A::Left );
}
namespace
{
class ScaleRenderer : public QskScaleRenderer
{
using Inherited = QskScaleRenderer;
public:
ScaleRenderer( const QskPlotView* view, QskPlot::Axis axis )
: m_view( view )
, m_axis( axis )
{
using Q = QskPlotView;
using A = QskAspect;
setEdge( axis == QskPlot::XBottom ? Qt::BottomEdge : Qt::LeftEdge );
setBoundaries( view->boundaries( axis ) );
setTickmarks( view->tickmarks( axis ) );
const auto aspect = Q::AxisScale
| ( ( axis == QskPlot::XBottom ) ? A::Bottom : A::Left );
const auto flags = view->flagHint< QskScaleRenderer::Flags >(
aspect | QskAspect::Style, QskScaleRenderer::Backbone );
setFlags( flags );
setTickColor( view->color( aspect ) );
const auto tickSize = view->strutSizeHint( aspect );
setTickWidth( tickSize.width() );
setTickLength( tickSize.height() );
setSpacing( view->spacingHint( aspect ) );
const auto fontRole = view->fontRoleHint( aspect );
setFont( view->effectiveSkin()->font( fontRole ) );
setTextColors( qskTextColors( view, aspect ) );
}
qreal labelOffset() const
{
qreal off = tickLength() + spacing();
if ( flags() & CenteredTickmarks )
off -= 0.5 * tickLength();
return off;
}
QVariant labelAt( qreal pos ) const override
{
return m_view->labelAt( m_axis, pos );
}
private:
const QskPlotView* m_view;
const QskPlot::Axis m_axis;
};
}
QskPlotViewSkinlet::QskPlotViewSkinlet( QskSkin* skin )
: Inherited( skin )
{
setNodeRoles( { PanelRole, CanvasRole, AxisRole } );
}
QskPlotViewSkinlet::~QskPlotViewSkinlet() = default;
QRectF QskPlotViewSkinlet::subControlRect( const QskSkinnable* skinnable,
const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const
{
using Q = QskPlotView;
if ( subControl == Q::Panel )
return contentsRect;
if ( subControl == Q::Canvas )
{
const auto view = static_cast< const QskPlotView* >( skinnable );
const auto rectX = sampleRect(
skinnable, contentsRect, Q::AxisScale, QskPlot::XBottom );
const auto rectY = sampleRect(
skinnable, contentsRect, Q::AxisScale, QskPlot::YLeft );
auto rect = view->subControlContentsRect( Q::Panel );
rect.setBottom( rectX.top() );
rect.setLeft( rectY.right() );
return rect;
}
return Inherited::subControlRect( skinnable, contentsRect, subControl );
}
QSGNode* QskPlotViewSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
using Q = QskPlotView;
switch ( nodeRole )
{
case PanelRole:
return updateBoxNode( skinnable, node, Q::Panel );
case CanvasRole:
return updateBoxNode( skinnable, node, Q::Canvas );
case AxisRole:
return updateSeriesNode( skinnable, Q::AxisScale, node );
}
return Inherited::updateSubNode( skinnable, nodeRole, node );
}
int QskPlotViewSkinlet::sampleCount(
const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const
{
using Q = QskPlotView;
if ( subControl == Q::AxisScale )
return 2; // for the moment only 2 axes
return Inherited::sampleCount( skinnable, subControl );
}
QRectF QskPlotViewSkinlet::sampleRect( const QskSkinnable* skinnable,
const QRectF& contentsRect, QskAspect::Subcontrol subControl, int index ) const
{
if ( subControl == QskPlotView::AxisScale )
{
const auto axis = static_cast< QskPlot::Axis >( index );
return axisRect( skinnable, axis );
}
return Inherited::sampleRect( skinnable, contentsRect, subControl, index );
}
QSGNode* QskPlotViewSkinlet::updateSampleNode( const QskSkinnable* skinnable,
QskAspect::Subcontrol subControl, int index, QSGNode* node ) const
{
using Q = QskPlotView;
if ( subControl == Q::AxisScale )
{
const auto axis = static_cast< QskPlot::Axis >( index );
return updateAxisNode( skinnable, axis, node );
}
return Inherited::updateSampleNode( skinnable, subControl, index, node );
}
QSizeF QskPlotViewSkinlet::sizeHint( const QskSkinnable* skinnable,
Qt::SizeHint which, const QSizeF& constraint ) const
{
return Inherited::sizeHint( skinnable, which, constraint );
}
QRectF QskPlotViewSkinlet::axisRect(
const QskSkinnable* skinnable, QskPlot::Axis axis ) const
{
using Q = QskPlotView;
using A = QskAspect;
const auto view = static_cast< const QskPlotView* >( skinnable );
auto rect = view->subControlContentsRect( Q::Panel );
const QskMargins paddingLeft = view->paddingHint( Q::AxisScale | A::Left );
const QskMargins paddingBottom = view->paddingHint( Q::AxisScale | A::Bottom );
qreal x0, y0;
{
const ScaleRenderer renderer( view, QskPlot::XBottom );
const auto sz = renderer.boundingLabelSize();
y0 = rect.bottom() - sz.height()
- renderer.labelOffset() - paddingBottom.height();
}
{
const ScaleRenderer renderer( view, QskPlot::YLeft );
const auto sz = renderer.boundingLabelSize();
x0 = rect.left() + sz.width()
+ renderer.labelOffset() + paddingLeft.width();
}
const auto canvasBorder = view->boxBorderMetricsHint( Q::Canvas );
if ( axis == QskPlot::XBottom )
{
rect.setTop( y0 );
rect.setLeft( x0 + canvasBorder.left() );
rect.setRight( rect.right() - canvasBorder.right() );
}
else
{
rect.setRight( x0 );
rect.setTop( rect.top() + canvasBorder.top() );
rect.setBottom( y0 - canvasBorder.bottom() );
}
return rect;
}
QSGNode* QskPlotViewSkinlet::updateAxisNode(
const QskSkinnable* skinnable, QskPlot::Axis axis, QSGNode* node ) const
{
using Q = QskPlotView;
const auto view = static_cast< const QskPlotView* >( skinnable );
const auto axisRect = sampleRect( view, view->contentsRect(), Q::AxisScale, axis );
const auto padding = view->paddingHint( qskAxisAspect( axis ) );
ScaleRenderer renderer( view, axis );
if ( axis == QskPlot::XBottom )
{
renderer.setPosition( axisRect.top() + padding.top() );
renderer.setRange( axisRect.left(), axisRect.right() );
}
else
{
renderer.setPosition( axisRect.right() - padding.right() );
renderer.setRange( axisRect.top(), axisRect.bottom() );
}
return renderer.updateNode( view, node );
}
#include "moc_QskPlotViewSkinlet.cpp"

View File

@ -0,0 +1,52 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#pragma once
#include <QskSkinlet.h>
#include <QskPlotNamespace.h>
class QskPlotViewSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole
{
PanelRole,
CanvasRole,
AxisRole
};
Q_INVOKABLE QskPlotViewSkinlet( QskSkin* = nullptr );
~QskPlotViewSkinlet() 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 final;
QRectF sampleRect( const QskSkinnable*,
const QRectF&, QskAspect::Subcontrol, int index ) const override;
protected:
QSGNode* updateSubNode( const QskSkinnable*,
quint8 nodeRole, QSGNode* ) const override;
QSGNode* updateSampleNode( const QskSkinnable*,
QskAspect::Subcontrol, int index, QSGNode* ) const override;
private:
QRectF axisRect( const QskSkinnable*, QskPlot::Axis ) const;
QSGNode* updateAxisNode( const QskSkinnable*,
QskPlot::Axis, QSGNode* ) const;
};

120
playground/plots/main.cpp Normal file
View File

@ -0,0 +1,120 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the 3-clause BSD License
*****************************************************************************/
#include "Plot.h"
#include <QskWindow.h>
#include <QskMainView.h>
#include <QskLinearBox.h>
#include <QskPushButton.h>
#include <QskObjectCounter.h>
#include <SkinnyShortcut.h>
#include <QGuiApplication>
namespace
{
class TestPlot : public Plot
{
public:
TestPlot( QQuickItem* parentItem = nullptr )
: Plot( parentItem )
{
setMargins( 5 );
QVector< Sample > samples;
samples += { -50, 30, 60, 80 };
samples += { -45, 35, 55, 85 };
samples += { -40, 32, 60, 77 };
samples += { -35, 38, 75, 66 };
samples += { -30, 40, 65, 75 };
samples += { -25, 48, 58, 82 };
samples += { -20, 27, 62, 85 };
samples += { -15, 32, 22, 70 };
samples += { -10, 30, 24, 77 };
samples += { -5, 20, 33, 70 };
samples += { 0, 40, 60, 80 };
setSamples( samples );
}
};
class Header : public QskLinearBox
{
Q_OBJECT
public:
Header( QQuickItem* parent = nullptr )
: QskLinearBox( Qt::Horizontal, parent )
{
initSizePolicy( QskSizePolicy::Ignored, QskSizePolicy::Fixed );
setPanel( true );
setPaddingHint( QskBox::Panel, 5 );
addStretch( 1 );
auto buttonLeft = new QskPushButton( "<", this );
buttonLeft->setAutoRepeat( true );
connect( buttonLeft, &QskPushButton::clicked,
this, [this] { Q_EMIT shiftClicked( +1 ); } );
auto buttonRight = new QskPushButton( ">", this );
buttonRight->setAutoRepeat( true );
connect( buttonRight, &QskPushButton::clicked,
this, [this] { Q_EMIT shiftClicked( -1 ); } );
auto buttonReset = new QskPushButton( "Reset", this );
connect( buttonReset, &QskPushButton::clicked,
this, &Header::resetClicked );
}
Q_SIGNALS:
void shiftClicked( int steps );
void resetClicked();
};
class MainView : public QskMainView
{
public:
MainView( QQuickItem* parent = nullptr )
: QskMainView( parent )
{
auto header = new Header( this );
auto plot = new TestPlot();
connect( header, &Header::resetClicked,
plot, &Plot::resetAxes );
connect( header, &Header::shiftClicked,
plot, &Plot::shiftXAxis );
setHeader( header );
setBody( plot );
}
};
}
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 MainView() );
window.resize( 800, 600 );
window.show();
return app.exec();
}
#include "main.moc"

View File

@ -99,6 +99,7 @@ list(APPEND SOURCES
list(APPEND HEADERS
nodes/QskArcNode.h
nodes/QskAxisScaleNode.h
nodes/QskBasicLinesNode.h
nodes/QskBoxNode.h
nodes/QskBoxClipNode.h
@ -126,7 +127,6 @@ list(APPEND HEADERS
nodes/QskTextNode.h
nodes/QskTextRenderer.h
nodes/QskTextureRenderer.h
nodes/QskTickmarksNode.h
nodes/QskVertex.h
)
@ -136,6 +136,7 @@ list(APPEND PRIVATE_HEADERS
list(APPEND SOURCES
nodes/QskArcNode.cpp
nodes/QskAxisScaleNode.cpp
nodes/QskBasicLinesNode.cpp
nodes/QskBoxNode.cpp
nodes/QskBoxClipNode.cpp
@ -163,7 +164,6 @@ list(APPEND SOURCES
nodes/QskTextNode.cpp
nodes/QskTextRenderer.cpp
nodes/QskTextureRenderer.cpp
nodes/QskTickmarksNode.cpp
nodes/QskVertex.cpp
)

View File

@ -7,18 +7,15 @@
#define QSK_GRADUATION_H
#include <QskGlobal.h>
#include <qmetatype.h>
class QskTickmarks;
namespace QskGraduation
{
Q_NAMESPACE_EXPORT( QSK_EXPORT )
QskTickmarks divideInterval( qreal x1, qreal x2,
QSK_EXPORT QskTickmarks divideInterval( qreal x1, qreal x2,
int maxMajorSteps, int maxMinorSteps, qreal stepSize = 0.0 );
qreal stepSize( double length, int numSteps );
QSK_EXPORT qreal stepSize( double length, int numSteps );
}
#endif

View File

@ -0,0 +1,207 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskAxisScaleNode.h"
#include "QskTickmarks.h"
#include "QskIntervalF.h"
#include <QTransform>
namespace
{
using Points = QSGGeometry::Point2D;
class Renderer
{
public:
inline Renderer( bool isHorizontal )
: m_isHorizontal( isHorizontal )
{
}
inline Points* addBackbone( Points* points,
qreal pos, qreal v1, qreal v2 ) const
{
if ( m_isHorizontal )
setLine( points, v1, pos, v2, pos );
else
setLine( points, pos, v1, pos, v2 );
return points + 2;
}
inline Points* addTickLine( Points* points,
qreal pos, qreal tick, qreal tickLength ) const
{
if ( m_isHorizontal )
setLine( points, tick, pos, tick, pos + tickLength );
else
setLine( points, pos, tick, pos + tickLength, tick );
return points + 2;
}
private:
inline void setLine( Points* points,
qreal x1, qreal y1, qreal x2, qreal y2 ) const
{
points[ 0 ].set( x1, y1 );
points[ 1 ].set( x2, y2 );
}
const bool m_isHorizontal;
};
}
class QskAxisScaleNode::PrivateData
{
public:
inline qreal map( qreal v ) const
{
if ( isHorizontal )
return transform.dx() + transform.m11() * v;
else
return transform.dy() + transform.m22() * v;
}
inline qreal length( QskTickmarks::TickType type ) const
{
switch( type )
{
case QskTickmarks::MinorTick:
return 0.7 * tickLength;
case QskTickmarks::MediumTick:
return 0.85 * tickLength;
default:
return tickLength;
}
}
inline qreal origin( qreal length ) const
{
switch( alignment )
{
case QskAxisScaleNode::Leading:
return pos - length;
case QskAxisScaleNode::Centered:
return pos - 0.5 * length;
default:
return pos;
}
}
bool isHorizontal = true;
qreal pos;
QskIntervalF backbone;
QTransform transform;
QskAxisScaleNode::Alignment alignment = QskAxisScaleNode::Centered;
qreal tickLength = 0.0;
QskHashValue hash = 0;
bool dirty = true;
};
QskAxisScaleNode::QskAxisScaleNode()
: m_data( new PrivateData() )
{
}
QskAxisScaleNode::~QskAxisScaleNode()
{
}
void QskAxisScaleNode::setAxis( Qt::Orientation orientation,
qreal pos, const QTransform& transform )
{
const bool isHorizontal = ( orientation == Qt::Horizontal );
if( isHorizontal != m_data->isHorizontal
|| pos != m_data->pos || transform != m_data->transform )
{
m_data->isHorizontal = isHorizontal;
m_data->pos = pos;
m_data->transform = transform;
m_data->dirty = true;
}
}
void QskAxisScaleNode::setTickGeometry(
Alignment alignment, qreal tickLength, qreal tickWidth )
{
setLineWidth( tickWidth );
if( tickLength != m_data->tickLength || alignment != m_data->alignment )
{
m_data->tickLength = tickLength;
m_data->alignment = alignment;
m_data->dirty = true;
}
}
void QskAxisScaleNode::update( const QskTickmarks& tickmarks,
const QskIntervalF& backbone )
{
const auto hash = tickmarks.hash( 17435 );
if ( m_data->hash != hash || m_data->backbone != backbone )
{
m_data->hash = hash;
m_data->backbone = backbone;
m_data->dirty = true;
}
if( !m_data->dirty )
return;
QSGGeometry::Point2D* points;
{
auto lineCount = tickmarks.tickCount();
if ( !backbone.isEmpty() )
lineCount++;
geometry()->allocate( lineCount * 2 );
points = geometry()->vertexDataAsPoint2D();
}
const Renderer renderer( m_data->isHorizontal );
if ( !m_data->backbone.isEmpty() )
{
const auto v1 = m_data->map( backbone.lowerBound() );
const auto v2 = m_data->map( backbone.upperBound() );
points = renderer.addBackbone( points, m_data->pos, v1, v2 );
}
for( int i = QskTickmarks::MinorTick;
i <= QskTickmarks::MajorTick; i++ )
{
const auto tickType = static_cast< QskTickmarks::TickType >( i );
const auto len = m_data->length( tickType );
const auto origin = m_data->origin( len );
const auto ticks = tickmarks.ticks( tickType );
for( auto tick : ticks )
{
tick = m_data->map( tick );
points = renderer.addTickLine( points, origin, tick, len );
}
}
geometry()->markVertexDataDirty();
markDirty( QSGNode::DirtyGeometry );
m_data->dirty = false;
}

View File

@ -0,0 +1,45 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#ifndef QSK_AXIS_SCALE_NODE_H
#define QSK_AXIS_SCALE_NODE_H
#include "QskGlobal.h"
#include "QskBasicLinesNode.h"
#include <qnamespace.h>
class QRectF;
class QskIntervalF;
class QskTickmarks;
class QskAxisScaleNodePrivate;
class QSK_EXPORT QskAxisScaleNode : public QskBasicLinesNode
{
using Inherited = QskBasicLinesNode;
public:
enum Alignment
{
Leading,
Centered,
Trailing
};
QskAxisScaleNode();
~QskAxisScaleNode() override;
void setAxis( Qt::Orientation, qreal pos, const QTransform& );
void setTickGeometry( Alignment, qreal tickLength, qreal tickWidth );
void update( const QskTickmarks&, const QskIntervalF& );
private:
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};
#endif

View File

@ -7,8 +7,7 @@
#include "QskTickmarks.h"
#include "QskSkinlet.h"
#include "QskSGNode.h"
#include "QskGraduationMetrics.h"
#include "QskTickmarksNode.h"
#include "QskAxisScaleNode.h"
#include "QskTextOptions.h"
#include "QskTextColors.h"
#include "QskGraphic.h"
@ -19,6 +18,31 @@
#include <qstring.h>
#include <qfontmetrics.h>
#include <qquickwindow.h>
namespace
{
class ScaleMap
{
public:
inline ScaleMap( bool isHorizontal, const QTransform& transform )
: t( isHorizontal ? transform.dx() : transform.dy() )
, f( isHorizontal ? transform.m11() : transform.m22() )
{
}
inline qreal map( qreal v ) const { return t + f * v; };
private:
const qreal t;
const qreal f;
};
}
static inline bool qskIsHorizontal( Qt::Edge edge )
{
return edge & ( Qt::TopEdge | Qt::BottomEdge );
}
static QSGNode* qskRemoveTraillingNodes( QSGNode* node, QSGNode* childNode )
{
@ -26,44 +50,74 @@ static QSGNode* qskRemoveTraillingNodes( QSGNode* node, QSGNode* childNode )
return nullptr;
}
static inline void qskInsertRemoveChild( QSGNode* parentNode,
QSGNode* oldNode, QSGNode* newNode, bool append )
static inline QTransform qskScaleTransform( Qt::Edge edge,
const QskIntervalF& boundaries, const QskIntervalF& range )
{
if ( newNode == oldNode )
return;
using T = QTransform;
if ( oldNode )
if ( qskIsHorizontal( edge ) )
{
parentNode->removeChildNode( oldNode );
if ( oldNode->flags() & QSGNode::OwnedByParent )
delete oldNode;
auto transform = T::fromTranslate( -boundaries.lowerBound(), 0.0 );
transform *= T::fromScale( range.length() / boundaries.length(), 1.0 );
transform *= T::fromTranslate( range.lowerBound(), 0.0 );
return transform;
}
else
{
auto transform = T::fromTranslate( 0.0, -boundaries.lowerBound() );
transform *= T::fromScale( 1.0, -range.length() / boundaries.length() );
transform *= T::fromTranslate( 0.0, range.upperBound() );
return transform;
}
}
static inline quint8 qskLabelNodeRole( const QVariant& label )
{
if ( !label.isNull() )
{
if ( label.canConvert< QString >() )
return 1;
if ( label.canConvert< QskGraphic >() )
return 2;
}
if ( newNode )
{
if ( append )
parentNode->appendChildNode( newNode );
else
parentNode->prependChildNode( newNode );
}
return QskSGNode::NoRole;
}
class QskScaleRenderer::PrivateData
{
public:
// Coordinates related to the scales
QskIntervalF boundaries;
QskTickmarks tickmarks;
QColor tickColor = Qt::black;
/*
Item cooordinates. In case of an horizontal scale
position is an y coordinate, while range corresponds to x coordinates
( vertical: v.v )
*/
qreal position = 0.0;
QskIntervalF range;
#if 1
QColor tickColor = Qt::black; // rgb value ???
#endif
qreal tickWidth = 1.0;
qreal tickLength = 10.0;
qreal spacing = 5.0;
QFont font;
QskTextColors textColors;
QskColorFilter colorFilter;
Qt::Orientation orientation = Qt::Horizontal;
Qt::Alignment alignment = Qt::AlignBottom | Qt::AlignRight;
Qt::Edge edge = Qt::BottomEdge;
QskScaleRenderer::Flags flags = ClampedLabels;
};
QskScaleRenderer::QskScaleRenderer()
@ -75,24 +129,32 @@ QskScaleRenderer::~QskScaleRenderer()
{
}
void QskScaleRenderer::setOrientation( Qt::Orientation orientation )
void QskScaleRenderer::setEdge( Qt::Edge edge )
{
m_data->orientation = orientation;
m_data->edge = edge;
}
Qt::Orientation QskScaleRenderer::orientation() const
Qt::Edge QskScaleRenderer::edge() const
{
return m_data->orientation;
return m_data->edge;
}
void QskScaleRenderer::setAlignment( Qt::Alignment alignment )
void QskScaleRenderer::setFlag( Flag flag, bool on )
{
m_data->alignment = alignment;
if ( on )
m_data->flags |= flag;
else
m_data->flags &= ~flag;
}
Qt::Alignment QskScaleRenderer::aligment() const
void QskScaleRenderer::setFlags( Flags flags )
{
return m_data->alignment;
m_data->flags = flags;
}
QskScaleRenderer::Flags QskScaleRenderer::flags() const
{
return m_data->flags;
}
void QskScaleRenderer::setBoundaries( qreal lowerBound, qreal upperBound )
@ -110,6 +172,31 @@ QskIntervalF QskScaleRenderer::boundaries() const
return m_data->boundaries;
}
qreal QskScaleRenderer::position() const
{
return m_data->position;
}
void QskScaleRenderer::setPosition( qreal pos )
{
m_data->position = pos;
}
void QskScaleRenderer::setRange( qreal from, qreal to )
{
setRange( QskIntervalF( from, to ) );
}
void QskScaleRenderer::setRange( const QskIntervalF& range )
{
m_data->range = range;
}
QskIntervalF QskScaleRenderer::range() const
{
return m_data->range;
}
void QskScaleRenderer::setTickmarks( const QskTickmarks& tickmarks )
{
m_data->tickmarks = tickmarks;
@ -120,6 +207,16 @@ const QskTickmarks& QskScaleRenderer::tickmarks() const
return m_data->tickmarks;
}
void QskScaleRenderer::setSpacing( qreal spacing )
{
m_data->spacing = qMax( spacing, 0.0 );
}
qreal QskScaleRenderer::spacing() const
{
return m_data->spacing;
}
void QskScaleRenderer::setTickColor( const QColor& color )
{
m_data->tickColor = color;
@ -130,9 +227,19 @@ QColor QskScaleRenderer::tickColor() const
return m_data->tickColor;
}
void QskScaleRenderer::setTickLength( qreal length )
{
m_data->tickLength = qMax( length, 0.0 );
}
qreal QskScaleRenderer::tickLength() const
{
return m_data->tickLength;
}
void QskScaleRenderer::setTickWidth( qreal width )
{
m_data->tickWidth = width;
m_data->tickWidth = qMax( width, 0.0 );
}
qreal QskScaleRenderer::tickWidth() const
@ -170,79 +277,74 @@ const QskColorFilter& QskScaleRenderer::colorFilter() const
return m_data->colorFilter;
}
QSGNode* QskScaleRenderer::updateScaleNode(
const QskSkinnable* skinnable, const QRectF& tickmarksRect,
const QRectF& labelsRect, QSGNode* node )
QSGNode* QskScaleRenderer::updateNode(
const QskSkinnable* skinnable, QSGNode* node )
{
enum Role
{
Ticks = 1,
Labels = 2
};
enum Role : quint8 { Ticks = 1, Labels = 2 };
static const QVector< quint8 > roles = { Ticks, Labels };
const auto transform = qskScaleTransform(
m_data->edge, m_data->boundaries, m_data->range );
if ( node == nullptr )
node = new QSGNode();
for ( auto role : roles )
{
QSGNode* oldNode = QskSGNode::findChildNode( node, Ticks );
QSGNode* newNode = nullptr;
auto oldNode = QskSGNode::findChildNode( node, role );
if ( !tickmarksRect.isEmpty() )
{
newNode = updateTicksNode( skinnable, tickmarksRect, oldNode );
if ( newNode )
QskSGNode::setNodeRole( newNode, Ticks );
}
auto newNode = ( role == Ticks )
? updateTicksNode( transform, oldNode )
: updateLabelsNode( skinnable, transform, oldNode );
qskInsertRemoveChild( node, oldNode, newNode, false );
}
{
QSGNode* oldNode = QskSGNode::findChildNode( node, Labels );
QSGNode* newNode = nullptr;
if ( !labelsRect.isEmpty() )
{
newNode = updateLabelsNode( skinnable, tickmarksRect, labelsRect, oldNode );
if ( newNode )
QskSGNode::setNodeRole( newNode, Labels );
}
qskInsertRemoveChild( node, oldNode, newNode, true );
QskSGNode::replaceChildNode( roles, role, node, oldNode, newNode );
}
return node;
}
QSGNode* QskScaleRenderer::updateTicksNode(
const QskSkinnable*, const QRectF& rect, QSGNode* node ) const
const QTransform& transform, QSGNode* node ) const
{
if ( rect.isEmpty() )
return nullptr;
QskIntervalF backbone;
if ( m_data->flags & Backbone )
backbone = m_data->boundaries;
auto ticksNode = static_cast< QskTickmarksNode* >( node );
const auto orientation = qskIsHorizontal( m_data->edge )
? Qt::Horizontal : Qt::Vertical;
if( ticksNode == nullptr )
ticksNode = new QskTickmarksNode;
auto alignment = QskAxisScaleNode::Centered;
#if 1
const int tickWidth = qRound( m_data->tickWidth );
#endif
if ( !( m_data->flags & CenteredTickmarks ) )
{
switch( m_data->edge )
{
case Qt::LeftEdge:
case Qt::TopEdge:
alignment = QskAxisScaleNode::Leading;
break;
case Qt::BottomEdge:
case Qt::RightEdge:
alignment = QskAxisScaleNode::Trailing;
break;
}
}
ticksNode->update( m_data->tickColor, rect, m_data->boundaries,
m_data->tickmarks, tickWidth, m_data->orientation,
m_data->alignment, {});
auto axisNode = QskSGNode::ensureNode< QskAxisScaleNode >( node );
return ticksNode;
axisNode->setColor( m_data->tickColor );
axisNode->setAxis( orientation, m_data->position, transform );
axisNode->setTickGeometry( alignment, m_data->tickLength, m_data->tickWidth );
axisNode->setPixelAlignment( Qt::Horizontal | Qt::Vertical );
axisNode->update( m_data->tickmarks, backbone );
return axisNode;
}
QSGNode* QskScaleRenderer::updateLabelsNode(
const QskSkinnable* skinnable, const QRectF& tickmarksRect,
const QRectF& labelsRect, QSGNode* node ) const
QSGNode* QskScaleRenderer::updateLabelsNode( const QskSkinnable* skinnable,
const QTransform& transform, QSGNode* node ) const
{
if ( labelsRect.isEmpty() || tickmarksRect.isEmpty() )
return nullptr;
const auto ticks = m_data->tickmarks.majorTicks();
if ( ticks.isEmpty() )
return nullptr;
@ -252,158 +354,72 @@ QSGNode* QskScaleRenderer::updateLabelsNode(
const QFontMetricsF fm( m_data->font );
const qreal length = ( m_data->orientation == Qt::Horizontal )
? tickmarksRect.width() : tickmarksRect.height();
const qreal ratio = length / m_data->boundaries.length();
auto nextNode = node->firstChild();
QRectF labelRect;
QRectF lastRect; // to skip overlapping label
for ( auto tick : ticks )
{
enum LabelNodeRole
{
TextNode = 1,
GraphicNode = 2
};
const auto label = labelAt( tick );
if ( label.isNull() )
continue;
const qreal tickPos = ratio * ( tick - m_data->boundaries.lowerBound() );
const auto role = qskLabelNodeRole( label );
if ( nextNode && QskSGNode::nodeRole( nextNode ) != role )
nextNode = qskRemoveTraillingNodes( node, nextNode );
QSizeF size;
if ( label.canConvert< QString >() )
{
auto text = label.toString();
if ( text.isEmpty() )
continue;
QRectF r;
Qt::Alignment alignment;
if( m_data->orientation == Qt::Horizontal )
{
const auto w = qskHorizontalAdvance( fm, text );
auto pos = tickmarksRect.x() + tickPos - 0.5 * w;
pos = qBound( labelsRect.left(), pos, labelsRect.right() - w );
r = QRectF( pos, labelsRect.y(), w, labelsRect.height() );
alignment = Qt::AlignLeft;
}
else
{
const auto h = fm.height();
auto pos = tickmarksRect.bottom() - ( tickPos + 0.5 * h );
/*
when clipping the label we can expand the clip rectangle
by the ascent/descent margins, as nothing gets painted there
anyway.
*/
const qreal min = labelsRect.top() - ( h - fm.ascent() );
const qreal max = labelsRect.bottom() + fm.descent();
pos = qBound( min, pos, max );
r = QRectF( labelsRect.x(), pos, labelsRect.width(), h );
alignment = Qt::AlignRight;
}
if ( nextNode && QskSGNode::nodeRole( nextNode ) != TextNode )
{
nextNode = qskRemoveTraillingNodes( node, nextNode );
}
if ( !labelRect.isEmpty() && labelRect.intersects( r ) )
{
if ( tick != ticks.last() )
{
text = QString();
}
else
{
if ( auto obsoleteNode = nextNode
? nextNode->previousSibling() : node->lastChild() )
{
node->removeChildNode( obsoleteNode );
if ( obsoleteNode->flags() & QSGNode::OwnedByParent )
delete obsoleteNode;
}
labelRect = r;
}
}
else
{
labelRect = r;
}
nextNode = QskSkinlet::updateTextNode( skinnable, nextNode,
r, alignment, text, m_data->font, QskTextOptions(),
m_data->textColors, Qsk::Normal );
if ( nextNode )
{
if ( nextNode->parent() != node )
{
QskSGNode::setNodeRole( nextNode, TextNode );
node->appendChildNode( nextNode );
}
nextNode = nextNode->nextSibling();
}
size = qskTextRenderSize( fm, label.toString() );
}
else if ( label.canConvert< QskGraphic >() )
{
const auto graphic = label.value< QskGraphic >();
if ( graphic.isNull() )
continue;
const auto h = fm.height();
const auto w = graphic.widthForHeight( h );
Qt::Alignment alignment;
if( m_data->orientation == Qt::Horizontal )
if ( !graphic.isNull() )
{
auto pos = tickmarksRect.x() + tickPos - 0.5 * w;
pos = qBound( labelsRect.left(), pos, labelsRect.right() - w );
labelRect = QRectF( pos, labelsRect.y(), w, h );
alignment = Qt::AlignHCenter | Qt::AlignBottom;
size.rheight() = fm.height();
size.rwidth() = graphic.widthForHeight( size.height() );
}
else
{
auto pos = tickmarksRect.bottom() - ( tickPos + 0.5 * h );
pos = qBound( labelsRect.top(), pos, labelsRect.bottom() - h );
}
labelRect = QRectF( labelsRect.right() - w, pos, w, h );
alignment = Qt::AlignRight | Qt::AlignVCenter;
if ( size.isEmpty() )
continue;
const auto rect = labelRect( transform, tick, size );
if ( !lastRect.isEmpty() && lastRect.intersects( rect ) )
{
/*
Label do overlap: in case it is the last tick we remove
the precessor - otherwise we simply skip this one
*/
if ( tick != ticks.last() )
continue; // skip this label
if ( auto obsoleteNode = nextNode
? nextNode->previousSibling() : node->lastChild() )
{
node->removeChildNode( obsoleteNode );
if ( obsoleteNode->flags() & QSGNode::OwnedByParent )
delete obsoleteNode;
}
}
nextNode = updateTickLabelNode( skinnable, nextNode, label, rect );
if ( nextNode)
{
lastRect = rect;
if ( nextNode->parent() != node )
{
QskSGNode::setNodeRole( nextNode, role );
node->appendChildNode( nextNode );
}
if ( nextNode && QskSGNode::nodeRole( nextNode ) != GraphicNode )
{
nextNode = qskRemoveTraillingNodes( node, nextNode );
}
nextNode = QskSkinlet::updateGraphicNode(
skinnable, nextNode, graphic, m_data->colorFilter, labelRect, alignment );
if ( nextNode )
{
if ( nextNode->parent() != node )
{
QskSGNode::setNodeRole( nextNode, GraphicNode );
node->appendChildNode( nextNode );
}
nextNode = nextNode->nextSibling();
}
nextNode = nextNode->nextSibling();
}
}
@ -417,40 +433,116 @@ QVariant QskScaleRenderer::labelAt( qreal pos ) const
return QString::number( pos, 'g' );
}
// should be cached
QSizeF QskScaleRenderer::boundingLabelSize() const
{
QSizeF boundingSize( 0.0, 0.0 );
const auto ticks = m_data->tickmarks.majorTicks();
if ( ticks.isEmpty() )
return QSizeF( 0.0, 0.0 );
return boundingSize;
const QFontMetricsF fm( m_data->font );
qreal maxWidth = 0.0;
const qreal h = fm.height();
for ( auto tick : ticks )
{
qreal w = 0.0;
const auto label = labelAt( tick );
if ( label.isNull() )
continue;
if ( label.canConvert< QString >() )
{
w = qskHorizontalAdvance( fm, label.toString() );
boundingSize = boundingSize.expandedTo(
qskTextRenderSize( fm, label.toString() ) );
}
else if ( label.canConvert< QskGraphic >() )
{
const auto graphic = label.value< QskGraphic >();
if ( !graphic.isNull() )
{
w = graphic.widthForHeight( h );
const auto w = graphic.widthForHeight( h );
boundingSize.setWidth( qMax( boundingSize.width(), w ) );
}
}
maxWidth = qMax( w, maxWidth );
}
return QSizeF( maxWidth, h );
return boundingSize;
}
QRectF QskScaleRenderer::labelRect(
const QTransform& transform, qreal tick, const QSizeF& labelSize ) const
{
const auto isHorizontal = qskIsHorizontal( m_data->edge );
auto offset = m_data->tickLength + m_data->spacing;
if ( m_data->flags & CenteredTickmarks )
offset -= 0.5 * m_data->tickLength;
const bool clampLabels = m_data->flags & ClampedLabels;
const qreal w = labelSize.width();
const qreal h = labelSize.height();
qreal x, y;
const ScaleMap map( isHorizontal, transform );
const auto tickPos = map.map( tick );
qreal min, max;
if ( clampLabels )
{
min = map.map( m_data->boundaries.lowerBound() );
max = map.map( m_data->boundaries.upperBound() );
}
if( isHorizontal )
{
x = tickPos - 0.5 * w;
if ( clampLabels )
x = qBound( min, x, max - w );
y = m_data->position + offset;
}
else
{
const auto tickPos = map.map( tick );
y = tickPos - 0.5 * h;
if ( clampLabels )
y = qBound( max, y, min - h );
x = m_data->position - offset - w;
}
return QRectF( x, y, w, h );
}
QSGNode* QskScaleRenderer::updateTickLabelNode( const QskSkinnable* skinnable,
QSGNode* node, const QVariant& label, const QRectF& rect ) const
{
if ( label.canConvert< QString >() )
{
return QskSkinlet::updateTextNode( skinnable, node,
rect, Qt::AlignCenter, label.toString(), m_data->font,
QskTextOptions(), m_data->textColors, Qsk::Normal );
}
if ( label.canConvert< QskGraphic >() )
{
const auto alignment = qskIsHorizontal( m_data->edge )
? ( Qt::AlignHCenter | Qt::AlignBottom )
: ( Qt::AlignRight | Qt::AlignVCenter );
return QskSkinlet::updateGraphicNode(
skinnable, node, label.value< QskGraphic >(),
m_data->colorFilter, rect, alignment );
}
return nullptr;
}
#include "moc_QskScaleRenderer.cpp"

View File

@ -23,30 +23,61 @@ class QskColorFilter;
class QSGNode;
class QVariant;
class QRectF;
class QPointF;
class QSizeF;
class QTransform;
class QSK_EXPORT QskScaleRenderer
{
Q_GADGET
public:
enum Flag
{
Backbone = 1 << 0,
CenteredTickmarks = 1 << 1,
ClampedLabels = 1 << 2
};
Q_ENUM( Flag )
Q_DECLARE_FLAGS( Flags, Flag )
QskScaleRenderer();
virtual ~QskScaleRenderer();
void setOrientation( Qt::Orientation );
Qt::Orientation orientation() const;
void setEdge( Qt::Edge );
Qt::Edge edge() const;
void setAlignment( Qt::Alignment );
Qt::Alignment aligment() const;
void setFlags( Flags );
Flags flags() const;
void setFlag( Flag, bool );
// scale coordinates
void setBoundaries( qreal lowerBound, qreal upperBound );
void setBoundaries( const QskIntervalF& );
QskIntervalF boundaries() const;
// item coordiates
qreal position() const;
void setPosition( qreal );
void setRange( qreal from, qreal to );
void setRange( const QskIntervalF& );
QskIntervalF range() const;
void setTickmarks( const QskTickmarks& );
const QskTickmarks& tickmarks() const;
void setSpacing( qreal );
qreal spacing() const;
void setTickColor( const QColor& );
QColor tickColor() const;
void setTickLength( qreal );
qreal tickLength() const;
void setTickWidth( qreal );
qreal tickWidth() const;
@ -59,24 +90,29 @@ class QSK_EXPORT QskScaleRenderer
void setColorFilter( const QskColorFilter& );
const QskColorFilter& colorFilter() const;
QSGNode* updateScaleNode( const QskSkinnable*,
const QRectF& tickmarksRect, const QRectF& labelsRect, QSGNode* );
QSGNode* updateNode( const QskSkinnable*, QSGNode* );
virtual QVariant labelAt( qreal pos ) const;
QSizeF boundingLabelSize() const;
virtual QSGNode* updateTicksNode(
const QskSkinnable*, const QRectF&, QSGNode* ) const;
protected:
virtual QSGNode* updateTicksNode( const QTransform&, QSGNode* ) const;
virtual QSGNode* updateLabelsNode(
const QskSkinnable*, const QRectF& ticksRect,
const QRectF& labelsRect, QSGNode* node ) const;
const QskSkinnable*, const QTransform&, QSGNode* ) const;
private:
Q_DISABLE_COPY( QskScaleRenderer )
QRectF labelRect( const QTransform&, qreal, const QSizeF& ) const;
QSGNode* updateTickLabelNode( const QskSkinnable*,
QSGNode*, const QVariant&, const QRectF& ) const;
class PrivateData;
std::unique_ptr< PrivateData > m_data;
};
Q_DECLARE_OPERATORS_FOR_FLAGS( QskScaleRenderer::Flags )
#endif

View File

@ -1,126 +0,0 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskTickmarksNode.h"
#include "QskTickmarks.h"
#include "QskGraduationMetrics.h"
#include <qrect.h>
#include <qhashfunctions.h>
QskTickmarksNode::QskTickmarksNode()
{
}
QskTickmarksNode::~QskTickmarksNode()
{
}
void QskTickmarksNode::update(
const QColor& color, const QRectF& rect,
const QskIntervalF& boundaries, const QskTickmarks& tickmarks,
int lineWidth, Qt::Orientation orientation, Qt::Alignment alignment,
const QskGraduationMetrics& graduationMetrics )
{
setLineWidth( lineWidth );
auto hash = tickmarks.hash( 17435 );
hash = graduationMetrics.hash( hash );
hash = qHashBits( &boundaries, sizeof( boundaries ), hash );
hash = qHashBits( &rect, sizeof( rect ), hash );
hash = qHash( orientation, hash );
hash = qHash( alignment, hash );
if ( hash != m_hash )
{
m_hash = hash;
geometry()->allocate( tickmarks.tickCount() * 2 );
auto vertexData = geometry()->vertexDataAsPoint2D();
const qreal min = boundaries.lowerBound();
const qreal range = boundaries.length();
using TM = QskTickmarks;
for( int i = TM::MinorTick; i <= TM::MajorTick; i++ )
{
const auto tickType = static_cast< TM::TickType >( i );
const auto ticks = tickmarks.ticks( tickType );
const float len = graduationMetrics.tickLength( tickType );
if ( orientation == Qt::Horizontal )
{
const qreal ratio = rect.width() / range;
for( const auto tick : ticks )
{
const auto x = rect.x() + ( tick - min ) * ratio;
qreal y1, y2;
if( alignment & Qt::AlignTop )
{
y1 = rect.top() + len;
y2 = rect.top();
}
else if( alignment & Qt::AlignVCenter )
{
const auto offset = ( rect.height() - len ) / 2;
y1 = rect.bottom() - offset;
y2 = rect.top() + offset;
}
else // Bottom (default)
{
y1 = rect.bottom();
y2 = rect.bottom() - len;
}
vertexData[ 0 ].set( x, y1 );
vertexData[ 1 ].set( x, y2 );
vertexData += 2;
}
}
else
{
const qreal ratio = rect.height() / range;
for( const auto tick : ticks )
{
const auto y = rect.bottom() - ( tick - min ) * ratio;
qreal x1, x2;
if( alignment & Qt::AlignLeft )
{
x1 = rect.left() + len;
x2 = rect.left();
}
else if( alignment & Qt::AlignHCenter )
{
const auto offset = ( rect.width() - len ) / 2;
x1 = rect.right() - offset;
x2 = rect.left() + offset;
}
else // Right (default)
{
x1 = rect.right();
x2 = rect.right() - len;
}
vertexData[ 0 ].set( x1, y );
vertexData[ 1 ].set( x2, y );
vertexData += 2;
}
}
}
geometry()->markVertexDataDirty();
markDirty( QSGNode::DirtyGeometry );
}
setColor( color );
}

View File

@ -1,30 +0,0 @@
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#ifndef QSK_TICKMARKS_NODE_H
#define QSK_TICKMARKS_NODE_H
#include "QskBasicLinesNode.h"
class QskIntervalF;
class QskTickmarks;
class QskGraduationMetrics;
class QRectF;
class QSK_EXPORT QskTickmarksNode : public QskBasicLinesNode
{
public:
QskTickmarksNode();
~QskTickmarksNode() override;
void update(const QColor&, const QRectF&, const QskIntervalF&,
const QskTickmarks&, int tickLineWidth, Qt::Orientation,
Qt::Alignment, const QskGraduationMetrics& );
private:
QskHashValue m_hash = 0;
};
#endif