feat(StatusChart): Adding chart component (#893)
Needed for https://github.com/status-im/status-desktop/issues/6490
This commit is contained in:
parent
44b6cda99a
commit
8be1af7059
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
|
@ -307,6 +307,11 @@ StatusWindow {
|
|||
selected: viewLoader.source.toString().includes(title)
|
||||
onClicked: mainPageView.page(title, true);
|
||||
}
|
||||
StatusNavigationListItem {
|
||||
title: "StatusChartPanel"
|
||||
selected: viewLoader.source.toString().includes(title)
|
||||
onClicked: mainPageView.page(title, true);
|
||||
}
|
||||
StatusListSectionHeadline { text: "StatusQ.Popup" }
|
||||
StatusNavigationListItem {
|
||||
title: "StatusPopupMenu"
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Components 0.1
|
||||
|
||||
import Sandbox 0.1
|
||||
|
||||
Item {
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
//dummy data
|
||||
property real stepSize: 1000
|
||||
property real minStep: 12000
|
||||
property real maxStep: 22000
|
||||
|
||||
property var graphTabsModel: [{text: "Price", enabled: true}, {text: "Balance", enabled: false}]
|
||||
property var timeRangeTabsModel: [{text: "1H", enabled: true},
|
||||
{text: "1D", enabled: true},{text: "7D", enabled: true},
|
||||
{text: "1M", enabled: true}, {text: "6M", enabled: true},
|
||||
{text: "1Y", enabled: true}, {text: "ALL", enabled: true}]
|
||||
|
||||
property var simTimer: Timer {
|
||||
running: true
|
||||
interval: 3000
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
d.generateData();
|
||||
}
|
||||
}
|
||||
|
||||
function minutes(minutes = 0) {
|
||||
var newMinute = new Date(new Date().getTime() - (minutes * 60 * 1000)).toString();
|
||||
if (newMinute.slice(10,12) === "00") {
|
||||
var dateToday = new Date(Date.now()).toString();
|
||||
return dateToday.slice(4,7) + " " + dateToday.slice(8,10);
|
||||
}
|
||||
return newMinute.slice(10,16);
|
||||
}
|
||||
|
||||
function hour(hours = 0) {
|
||||
var newHour = new Date(new Date().getTime() - (hours * 60 * 60 * 1000)).toString();
|
||||
if (newHour.slice(10,12) === "00") {
|
||||
var dateToday = new Date(Date.now()).toString();
|
||||
return dateToday.slice(4,7) + " " + dateToday.slice(8,10);
|
||||
}
|
||||
return newHour.slice(10,16);
|
||||
}
|
||||
|
||||
function day(before = 0) {
|
||||
var newDay = new Date(Date.now() - before * 24 * 60 * 60 * 1000).toString();
|
||||
return newDay.slice(4,7) + " " + newDay.slice(8,10);
|
||||
}
|
||||
|
||||
function month(before = 0) {
|
||||
var newMonth = new Date(Date.now() - before * 24 * 60 * 60 * 1000).toString();
|
||||
return newMonth.slice(4,7) + " '" + newMonth.slice(newMonth.indexOf("G")-3, newMonth.indexOf("G")-1);
|
||||
}
|
||||
|
||||
property var timeRange: [
|
||||
{'1H': [minutes(60), minutes(55), minutes(50), minutes(45), minutes(40), minutes(35), minutes(30), minutes(25), minutes(20), minutes(15), minutes(10), minutes(5), minutes()]},
|
||||
{'1D': [hour(24), hour(23), hour(22), hour(21), hour(20), hour(19), hour(18), hour(17), hour(16), hour(15), hour(14), hour(13),
|
||||
hour(12), hour(11), hour(10), hour(9), hour(8), hour(7), hour(6), hour(5), hour(4), hour(3), hour(2), hour(1), hour()]},
|
||||
{'7D': [day(6), day(5), day(4), day(3), day(2), day(1), day()]},
|
||||
{'1M': [day(30), day(28), day(26), day(24), day(22), day(20), day(18), day(16), day(14), day(12), day(10), day(8), day(6), day(4), day()]},
|
||||
{'6M': [month(150), month(120), month(90), month(60), month(30), month()]},
|
||||
{'1Y': [month(330), month(300), month(270), month(240), month(210), month(180), month(150), month(120), month(90), month(60), month(30), month()]},
|
||||
{'ALL': ['2016', '2017', '2018', '2019', '2020', '2021', '2022']}
|
||||
]
|
||||
|
||||
function generateData() {
|
||||
var result = [];
|
||||
for (var i = 0; i < timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange].length; ++i) {
|
||||
result[i] = Math.random() * (maxStep - minStep) + minStep;
|
||||
}
|
||||
graphDetail.chart.chartData.datasets[0].data = result;
|
||||
graphDetail.chart.animateToNewData();
|
||||
}
|
||||
}
|
||||
|
||||
StatusChartPanel {
|
||||
id: graphDetail
|
||||
height: 290
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 24
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
graphsModel: d.graphTabsModel
|
||||
timeRangeModel: d.timeRangeTabsModel
|
||||
onHeaderTabClicked: {
|
||||
//TODO
|
||||
//if time range tab
|
||||
d.generateData();
|
||||
//if graph bar
|
||||
//switch graph
|
||||
}
|
||||
chart.chartType: 'line'
|
||||
chart.chartData: {
|
||||
return {
|
||||
labels: d.timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
|
||||
datasets: [{
|
||||
label: 'Price',
|
||||
xAxisId: 'x-axis-1',
|
||||
yAxisId: 'y-axis-1',
|
||||
backgroundColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 0.2)' : 'rgba(67, 96, 223, 0.2)',
|
||||
borderColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 1)' : 'rgba(67, 96, 223, 1)',
|
||||
borderWidth: 3,
|
||||
pointRadius: 0,
|
||||
//data: d.generateData()
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
chart.chartOptions: {
|
||||
return {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
//TODO enable zoom
|
||||
// zoom: {
|
||||
// enabled: true,
|
||||
// drag: true,
|
||||
// speed: 0.1,
|
||||
// threshold: 2
|
||||
// },
|
||||
// pan:{enabled:true,mode:'x'},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
footer: function(tooltipItem, data) { return 'Vol: $43,042,678,876'; },
|
||||
label: function(tooltipItem, data) {
|
||||
let label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += tooltipItem.yLabel.toFixed(2);
|
||||
return label.slice(0,label.indexOf(":")+1)+ " $"+label.slice(label.indexOf(":")+2, label.length);
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
id: 'x-axis-1',
|
||||
position: 'bottom',
|
||||
gridLines: {
|
||||
drawOnChartArea: false,
|
||||
drawBorder: false,
|
||||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
fontSize: 10,
|
||||
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
|
||||
padding: 16,
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'left',
|
||||
id: 'y-axis-1',
|
||||
gridLines: {
|
||||
borderDash: [8, 4],
|
||||
drawBorder: false,
|
||||
drawTicks: false,
|
||||
color: (Theme.palette.name === "dark") ? '#909090' : '#939BA1'
|
||||
},
|
||||
beforeDataLimits: (axis) => {
|
||||
axis.paddingTop = 25;
|
||||
axis.paddingBottom = 0;
|
||||
},
|
||||
ticks: {
|
||||
fontSize: 10,
|
||||
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
|
||||
padding: 24,
|
||||
min: d.minStep,
|
||||
max: d.maxStep,
|
||||
stepSize: d.stepSize,
|
||||
callback: function(value, index, ticks) {
|
||||
return '$' + value;
|
||||
},
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import StatusQ.Controls 0.1
|
||||
|
||||
import "private/chart"
|
||||
|
||||
/*!
|
||||
\qmltype StatusChartPanel
|
||||
\inherits Page
|
||||
\inqmlmodule StatusQ.Components
|
||||
\since StatusQ.Components 0.1
|
||||
\brief Displays a chart component together with an optional header in order to add a list of options.
|
||||
Inherits \l{https://doc.qt.io/qt-5/qml-qtquick-controls2-page.html}{Page}.
|
||||
|
||||
The \c StatusChartPanel displays a customizable chart component. Additionally, options
|
||||
can be added in the header in both right and left sides given graphTabsModel and timeRangeTabsModel
|
||||
property are set respectively.
|
||||
|
||||
For example:
|
||||
|
||||
\qml
|
||||
StatusChartPanel {
|
||||
id: graphDetail
|
||||
width: parent.width
|
||||
height: 290
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
graphsModel: d.graphTabsModel
|
||||
timeRangeModel: d.timeRangeTabsModel
|
||||
chart.chartType: 'line'
|
||||
chart.chartData: {
|
||||
return {
|
||||
datasets: [{
|
||||
data: d.data
|
||||
}],
|
||||
...
|
||||
}
|
||||
}
|
||||
chart.chartOptions: {
|
||||
return {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
\endqml
|
||||
|
||||
\image status_chart_panel.png
|
||||
|
||||
For a list of components available see StatusQ.
|
||||
*/
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
/*!
|
||||
\qmlproperty real StatusChartPanel::graphsModel
|
||||
This property holds the graphs model options to be set on the left side tab bar of the header.
|
||||
*/
|
||||
property var graphsModel
|
||||
/*!
|
||||
\qmlproperty real StatusChartPanel::timeRangeModel
|
||||
This property holds the time range options to be set on the right side tab bar of the header.
|
||||
*/
|
||||
property var timeRangeModel
|
||||
|
||||
/*!
|
||||
\qmlproperty real StatusChartPanel::graphComponent
|
||||
This property holds holds a reference to the graph component.
|
||||
*/
|
||||
property alias chart: graphComponent
|
||||
|
||||
/*!
|
||||
\qmlproperty real StatusChartPanel::timeRangeTabBarIndex
|
||||
This property holds holds a reference to the time range tab bar current index.
|
||||
*/
|
||||
property alias timeRangeTabBarIndex: timeRangeTabBar.currentIndex
|
||||
|
||||
/*!
|
||||
\qmlproperty real StatusChartPanel::selectedTimeRange
|
||||
This property holds holds the text of the current time range tab bar selected tab.
|
||||
*/
|
||||
property string selectedTimeRange: timeRangeTabBar.currentItem.text
|
||||
|
||||
/*!
|
||||
\qmlsignal
|
||||
This signal is emitted when a header tab bar is clicked.
|
||||
*/
|
||||
signal headerTabClicked(string text)
|
||||
|
||||
Component {
|
||||
id: tabButton
|
||||
StatusTabButton {
|
||||
leftPadding: 0
|
||||
width: implicitWidth
|
||||
onClicked: {
|
||||
root.headerTabClicked(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!!timeRangeModel) {
|
||||
for (var i = 0; i < timeRangeModel.length; i++) {
|
||||
var timeTab = tabButton.createObject(root, { text: qsTr(timeRangeModel[i].text.toString()),
|
||||
enabled: timeRangeModel[i].enabled });
|
||||
timeRangeTabBar.addItem(timeTab);
|
||||
}
|
||||
}
|
||||
if (!!graphsModel) {
|
||||
for (var j = 0; j < graphsModel.length; j++) {
|
||||
var graphTab = tabButton.createObject(root, { text: qsTr(graphsModel[j].text.toString()),
|
||||
enabled: graphsModel[j].enabled });
|
||||
graphsTabBar.addItem(graphTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
header: Item {
|
||||
height: childrenRect.height
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 40
|
||||
anchors.right: parent.right
|
||||
StatusTabBar {
|
||||
id: graphsTabBar
|
||||
}
|
||||
StatusTabBar {
|
||||
id: timeRangeTabBar
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
Chart {
|
||||
id: graphComponent
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,139 @@
|
|||
/*!
|
||||
* Elypson's Chart.qml adaptor to Chart.js
|
||||
* (c) 2020 ChartJs2QML contributors (starting with Elypson, Michael A. Voelkel, https://github.com/Elypson)
|
||||
* Released under the MIT License
|
||||
*/
|
||||
|
||||
import QtQuick 2.13
|
||||
import "Chart.js" as Chart
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
|
||||
property var jsChart: undefined
|
||||
property string chartType
|
||||
property var chartData
|
||||
property var chartOptions
|
||||
property double chartAnimationProgress: 0.1
|
||||
property var animationEasingType: Easing.InOutExpo
|
||||
property double animationDuration: 500
|
||||
property var memorizedContext
|
||||
property var memorizedData
|
||||
property var memorizedOptions
|
||||
property alias animationRunning: chartAnimator.running
|
||||
|
||||
signal animationFinished()
|
||||
|
||||
function animateToNewData()
|
||||
{
|
||||
chartAnimationProgress = 0.1;
|
||||
jsChart.update();
|
||||
chartAnimator.restart();
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: event
|
||||
anchors.fill: root
|
||||
hoverEnabled: true
|
||||
enabled: true
|
||||
property var handler: undefined
|
||||
|
||||
property QtObject mouseEvent: QtObject {
|
||||
property int left: 0
|
||||
property int top: 0
|
||||
property int x: 0
|
||||
property int y: 0
|
||||
property int clientX: 0
|
||||
property int clientY: 0
|
||||
property string type: ""
|
||||
property var target
|
||||
}
|
||||
|
||||
function submitEvent(mouse, type) {
|
||||
mouseEvent.type = type
|
||||
mouseEvent.clientX = mouse ? mouse.x : 0;
|
||||
mouseEvent.clientY = mouse ? mouse.y : 0;
|
||||
mouseEvent.x = mouse ? mouse.x : 0;
|
||||
mouseEvent.y = mouse ? mouse.y : 0;
|
||||
mouseEvent.left = 0;
|
||||
mouseEvent.top = 0;
|
||||
mouseEvent.target = root;
|
||||
|
||||
if(handler) {
|
||||
handler(mouseEvent);
|
||||
}
|
||||
|
||||
root.requestPaint();
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
submitEvent(mouse, "click");
|
||||
}
|
||||
onPositionChanged: {
|
||||
submitEvent(mouse, "mousemove");
|
||||
}
|
||||
onExited: {
|
||||
submitEvent(undefined, "mouseout");
|
||||
}
|
||||
onEntered: {
|
||||
submitEvent(undefined, "mouseenter");
|
||||
}
|
||||
onPressed: {
|
||||
submitEvent(mouse, "mousedown");
|
||||
}
|
||||
onReleased: {
|
||||
submitEvent(mouse, "mouseup");
|
||||
}
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
id: chartAnimator
|
||||
target: root
|
||||
property: "chartAnimationProgress"
|
||||
alwaysRunToEnd: true
|
||||
to: 1
|
||||
duration: root.animationDuration
|
||||
easing.type: root.animationEasingType
|
||||
onFinished: {
|
||||
root.animationFinished();
|
||||
}
|
||||
}
|
||||
|
||||
onChartAnimationProgressChanged: {
|
||||
root.requestPaint();
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
if(root.getContext('2d') != null && memorizedContext != root.getContext('2d') || memorizedData != root.chartData || memorizedOptions != root.chartOptions) {
|
||||
var ctx = root.getContext('2d');
|
||||
|
||||
jsChart = new Chart.build(ctx, {
|
||||
type: root.chartType,
|
||||
data: root.chartData,
|
||||
options: root.chartOptions
|
||||
});
|
||||
|
||||
memorizedData = root.chartData ;
|
||||
memorizedContext = root.getContext('2d');
|
||||
memorizedOptions = root.chartOptions;
|
||||
|
||||
root.jsChart.bindEvents(function(newHandler) {event.handler = newHandler;});
|
||||
|
||||
chartAnimator.start();
|
||||
}
|
||||
|
||||
jsChart.draw(chartAnimationProgress);
|
||||
}
|
||||
|
||||
onWidthChanged: {
|
||||
if(jsChart) {
|
||||
jsChart.resize();
|
||||
}
|
||||
}
|
||||
|
||||
onHeightChanged: {
|
||||
if(jsChart) {
|
||||
jsChart.resize();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 ChartJs2QML contributors (starting with Elypson, Michael A. Voelkel, https://github.com/Elypson)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -43,3 +43,5 @@ StatusCommunityTags 0.1 StatusCommunityTags.qml
|
|||
StatusItemSelector 0.1 StatusItemSelector.qml
|
||||
StatusCard 0.1 StatusCard.qml
|
||||
StatusDatePicker 0.1 StatusDatePicker.qml
|
||||
StatusChart 0.1 StatusChart.qml
|
||||
StatusChartPanel 0.1 StatusChartPanel.qml
|
||||
|
|
Loading…
Reference in New Issue