feat(communityChart): Add messages over time chart - mocked data

Implementing community messages over time chart
Adding `OverviewSettingsChart.qml` to storybook
This commit is contained in:
Alex Jbanca 2023-07-14 13:34:23 +03:00 committed by Alex Jbanca
parent 1a5907e22c
commit bed7db5528
9 changed files with 532 additions and 26 deletions

View File

@ -201,6 +201,10 @@ ListModel {
title: "StatusInfoBoxPanel" title: "StatusInfoBoxPanel"
section: "Panels" section: "Panels"
} }
ListElement {
title: "OverviewSettingsChart"
section: "Panels"
}
ListElement { ListElement {
title: "BurnTokensPopup" title: "BurnTokensPopup"
section: "Popups" section: "Popups"

View File

@ -236,5 +236,8 @@
], ],
"OwnerTokenWelcomeView": [ "OwnerTokenWelcomeView": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?type=design&node-id=34794%3A590064&mode=design&t=eabTmd6JZbuycoy8-1" "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?type=design&node-id=34794%3A590064&mode=design&t=eabTmd6JZbuycoy8-1"
],
"OverviewSettingsChart": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?type=design&node-id=31281-635619&mode=design&t=RYpVRgwqCjp8fUEX-0"
] ]
} }

View File

@ -0,0 +1,30 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import AppLayouts.Communities.panels 1.0
import Models 1.0
SplitView {
OverviewSettingsChart {
id: chart
SplitView.fillWidth: true
SplitView.fillHeight: true
model: generateRandomModel()
}
function generateRandomModel() {
var newModel = []
const now = Date.now()
for(var i = 0; i < 500000; i++) {
var date = generateRandomDate(1463154962000, now)
newModel.push(date)
}
return newModel
}
function generateRandomDate(from, to) {
return from + Math.random() * (to - from)
}
}

View File

@ -97,6 +97,18 @@ Page {
*/ */
property int defaultTimeRangeIndexShown: 0 property int defaultTimeRangeIndexShown: 0
/*!
\qmlproperty int StatusChartPanel::headerLeftPadding
This property holds the left padding of the header.
*/
property int headerLeftPadding: 46
/*!
\qmlproperty int StatusChartPanel::headerBottomPadding
This property holds the bottom padding of the header.
*/
property int headerBottomPadding: 0
/*! /*!
\qmlsignal \qmlsignal
This signal is emitted when a header tab bar is clicked. This signal is emitted when a header tab bar is clicked.
@ -141,10 +153,10 @@ Page {
background: null background: null
header: Item { header: Item {
height: childrenRect.height height: childrenRect.height + root.headerBottomPadding
RowLayout { RowLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 46 anchors.leftMargin: root.headerLeftPadding
anchors.right: parent.right anchors.right: parent.right
StatusTabBar { StatusTabBar {
id: graphsTabBar id: graphsTabBar

View File

@ -10239,7 +10239,7 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
// Invoke onHover hook // Invoke onHover hook
// Need to call with native event here to not break backwards compatibility // Need to call with native event here to not break backwards compatibility
helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active, e], me);
if (e.type === 'mouseup' || e.type === 'click') { if (e.type === 'mouseup' || e.type === 'click') {
if (options.onClick) { if (options.onClick) {

View File

@ -248,6 +248,101 @@ QtObject {
return Math.round((d1 - d2) / d.msInADay) // Math.round: not all days are 24 hours long! return Math.round((d1 - d2) / d.msInADay) // Math.round: not all days are 24 hours long!
} }
/**
* Returns the timestamp of the last minute
* - `before``: number of minutes before the reference minute
* - `time``: timestamp to use as reference
* - `rounding``: if true, rounds to the last minute
**/
function minutes(before = 0, time = Date.now(), rounding = true) {
let timestamp = rounding ? Math.floor(time / minutesToMs(5)) * minutesToMs(5)
: time
return timestamp - minutesToMs(before)
}
/**
* Returns the timestamp of the last hour
* - `before``: number of hours before the reference hour
* - `time``: timestamp to use as reference
* - `rounding``: if true, rounds to the last hour
**/
function hours(before = 0, time = Date.now(), rounding = true) {
let timestamp = rounding ? Math.floor(time / minutesToMs(30)) * minutesToMs(30)
: time
return timestamp - hoursToMs(before)
}
/**
* Returns the timestamp of the last day
* - `before``: number of days before the reference day
* - `time``: timestamp to use as reference
* - `rounding``: if true, rounds to the last day
**/
function days(before = 0, time = Date.now(), rounding = true) {
let date = new Date(time)
if(rounding) {
date = new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
date.setDate(date.getDate() - before)
return date.getTime()
}
/**
* Returns the timestamp of the last week
* - `before``: number of weeks before the reference week
* - `time``: timestamp to use as reference
* - `rounding``: if true, rounds to the last week
**/
function months(before = 0, time = Date.now(), rounding = true) {
let date = new Date(time)
if(rounding) {
date = new Date(date.getFullYear(), date.getMonth(), 1)
}
date.setMonth(date.getMonth() - before)
return date.getTime()
}
/**
* Returns the timestamp of the last year
* - `before``: number of years before the reference year
* - `time``: timestamp to use as reference
* - `rounding``: if true, rounds to the last year
**/
function years(before = 0, time = Date.now(), rounding = true) {
let date = new Date(time)
if(rounding) {
date = new Date(date.getFullYear(), 0, 1)
}
date.setFullYear(date.getFullYear() - before)
return date.getTime()
}
/**
* Retuns the number of milliseconds in the given amount of minutes
* - `count``: number of minutes
**/
function minutesToMs(count = 1) {
return count * 60 * 1000
}
/**
* Retuns the number of milliseconds in the given amount of hours
* - `count``: number of hours
**/
function hoursToMs(count = 1) {
return count * minutesToMs(60)
}
/**
* Retuns the number of milliseconds in the given amount of days
* - `count``: number of days
**/
function daysToMs(count = 1) {
return count * hoursToMs(24)
}
/** /**
Converts the Date to a string containing the date suitable for the specified locale in the specified format. Converts the Date to a string containing the date suitable for the specified locale in the specified format.

View File

@ -0,0 +1,371 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import utils 1.0
StatusChartPanel {
id: root
/**
* Flat model to use for the chart containing timestamps
* type: {Array}
*/
property var model: []
QtObject {
id: d
//visual properties
readonly property string baseColor1: Theme.palette.baseColor1
readonly property string twentyPercentBaseColor1: Theme.palette.alphaColor(baseColor1, 0.2)
readonly property string barColor: Theme.palette.primaryColor2
readonly property string barBorderColor: Theme.palette.primaryColor1
readonly property string messagesLabel: qsTr("Messages")
property int hoveredBarIndex: 0
property int hoveredBarValue: 0
readonly property var hoveredModelMetadata: modelMetadata[root.timeRangeTabBarIndex].modelItems[hoveredBarIndex]
readonly property var tooltipConfig: modelMetadata[root.timeRangeTabBarIndex].tooltipConfig
readonly property var graphTabsModel: [{text: messagesLabel, enabled: true}]
readonly property var now: Date.now()
readonly property var chartData: selectedTabInfo.modelItems.map(x => d.itemsCountInRange(root.model, x.start, x.end))
readonly property var labels: selectedTabInfo.modelItems.map(x => x.label)
readonly property var selectedTabInfo: modelMetadata[root.timeRangeTabBarIndex]
readonly property var modelMetadata: [
{
text: qsTr("1H"),
modelItems: [
{ start: LocaleUtils.minutes(60, now), end: LocaleUtils.minutes(50, now), label: minutesStr(55)},
{ start: LocaleUtils.minutes(50, now), end: LocaleUtils.minutes(40, now), label: minutesStr(45)},
{ start: LocaleUtils.minutes(40, now), end: LocaleUtils.minutes(30, now), label: minutesStr(35)},
{ start: LocaleUtils.minutes(30, now), end: LocaleUtils.minutes(20, now), label: minutesStr(25)},
{ start: LocaleUtils.minutes(20, now), end: LocaleUtils.minutes(10, now), label: minutesStr(15)},
{ start: LocaleUtils.minutes(10, now), end: LocaleUtils.minutes(0, now, false), label: minutesStr(5)}
],
tooltipConfig: {
timeRangeString: qsTr("Time period"),
timeRangeFormatter: d.hoursRangeStr
},
},
{
text: qsTr("1D"),
modelItems: [
{ start: LocaleUtils.hours(24, now), end: LocaleUtils.hours(20, now), label: hourStr(22)},
{ start: LocaleUtils.hours(20, now), end: LocaleUtils.hours(16, now), label: hourStr(18)},
{ start: LocaleUtils.hours(16, now), end: LocaleUtils.hours(12, now), label: hourStr(14)},
{ start: LocaleUtils.hours(12, now), end: LocaleUtils.hours(8, now), label: hourStr(10)},
{ start: LocaleUtils.hours(8, now), end: LocaleUtils.hours(4, now), label: hourStr(6)},
{ start: LocaleUtils.hours(4, now), end: LocaleUtils.hours(0, now, false), label: hourStr(2)}
],
tooltipConfig: {
timeRangeString: qsTr("Time period"),
timeRangeFormatter: d.hoursRangeStr
},
},
{
text: qsTr("7D"),
modelItems: [
{ start: LocaleUtils.days(6, now), end: LocaleUtils.days(5, now), label: dayStr(6)},
{ start: LocaleUtils.days(5, now), end: LocaleUtils.days(4, now), label: dayStr(5)},
{ start: LocaleUtils.days(4, now), end: LocaleUtils.days(3, now), label: dayStr(4)},
{ start: LocaleUtils.days(3, now), end: LocaleUtils.days(2, now), label: dayStr(3)},
{ start: LocaleUtils.days(2, now), end: LocaleUtils.days(1, now), label: dayStr(2)},
{ start: LocaleUtils.days(1, now), end: LocaleUtils.days(0, now), label: dayStr(1)},
{ start: LocaleUtils.days(0, now), end: LocaleUtils.days(0, now, false), label: dayStr(0)}
],
tooltipConfig: {
timeRangeString: qsTr("Date"),
timeRangeFormatter: d.daysRangeStr
},
},
{
text: qsTr("1M"),
modelItems: [
{ start: LocaleUtils.days(30, now), end: LocaleUtils.days(25, now), label: dayStr(30)},
{ start: LocaleUtils.days(25, now), end: LocaleUtils.days(20, now), label: dayStr(25)},
{ start: LocaleUtils.days(20, now), end: LocaleUtils.days(15, now), label: dayStr(20)},
{ start: LocaleUtils.days(15, now), end: LocaleUtils.days(10, now), label: dayStr(15)},
{ start: LocaleUtils.days(10, now), end: LocaleUtils.days(5, now), label: dayStr(10)},
{ start: LocaleUtils.days(5, now), end: LocaleUtils.days(0, now, false), label: dayStr(5)}
],
tooltipConfig: {
timeRangeString: qsTr("Time period"),
timeRangeFormatter: d.daysRangeStr
},
},
{
text: qsTr("6M"),
modelItems: [
{ start: LocaleUtils.months(5, now), end: LocaleUtils.months(4, now), label: monthStr(5)},
{ start: LocaleUtils.months(4, now), end: LocaleUtils.months(3, now), label: monthStr(4)},
{ start: LocaleUtils.months(3, now), end: LocaleUtils.months(2, now), label: monthStr(3)},
{ start: LocaleUtils.months(2, now), end: LocaleUtils.months(1, now), label: monthStr(2)},
{ start: LocaleUtils.months(1, now), end: LocaleUtils.months(0, now), label: monthStr(1)},
{ start: LocaleUtils.months(0, now), end: LocaleUtils.months(0, now, false), label: monthStr(0)}
],
tooltipConfig: {
timeRangeString: qsTr("Month"),
timeRangeFormatter: d.monthsRangeStr
},
},
{
text: qsTr("1Y"),
modelItems: [
{ start: LocaleUtils.months(12, now), end: LocaleUtils.months(10, now), label: monthStr(11)},
{ start: LocaleUtils.months(10, now), end: LocaleUtils.months(8, now), label: monthStr(9)},
{ start: LocaleUtils.months(8, now), end: LocaleUtils.months(6, now), label: monthStr(7)},
{ start: LocaleUtils.months(6, now), end: LocaleUtils.months(4, now), label: monthStr(5)},
{ start: LocaleUtils.months(4, now), end: LocaleUtils.months(2, now), label: monthStr(3)},
{ start: LocaleUtils.months(2, now), end: LocaleUtils.months(0, now, false), label: monthStr(1)}
],
tooltipConfig: {
timeRangeString: qsTr("Time period"),
timeRangeFormatter: d.monthsRangeStr
},
},
{
text: qsTr("ALL"),
modelItems: [
{ start: LocaleUtils.years(7, now), end: LocaleUtils.years(6, now), label: yearsStr(7) },
{ start: LocaleUtils.years(6, now), end: LocaleUtils.years(5, now), label: yearsStr(6) },
{ start: LocaleUtils.years(5, now), end: LocaleUtils.years(4, now), label: yearsStr(5) },
{ start: LocaleUtils.years(4, now), end: LocaleUtils.years(3, now), label: yearsStr(4) },
{ start: LocaleUtils.years(3, now), end: LocaleUtils.years(2, now), label: yearsStr(3) },
{ start: LocaleUtils.years(2, now), end: LocaleUtils.years(1, now), label: yearsStr(2) },
{ start: LocaleUtils.years(1, now), end: LocaleUtils.years(0, now), label: yearsStr(1) },
{ start: LocaleUtils.years(0, now), end: LocaleUtils.years(0, now, false), label: yearsStr(0) }
],
tooltipConfig: {
timeRangeString: qsTr("Year"),
timeRangeFormatter: d.yearsRangeStr
},
}
]
function itemsCountInRange(array, start, end) {
return array ? array.filter(x => x <= end && x > start).length : 0
}
function minutesStr(before = 0, timeReference = now, roundCurrentTime = true) {
return LocaleUtils.formatTime(LocaleUtils.minutes(before, timeReference, roundCurrentTime), Locale.ShortFormat)
}
function hourStr(before = 0, timeReference = now, roundCurrentTime = true) {
return LocaleUtils.formatTime(LocaleUtils.hours(before, timeReference, roundCurrentTime), Locale.ShortFormat)
}
function dayStr(before = 0, timeReference = now, roundCurrentTime = true) {
return LocaleUtils.getDayMonth(LocaleUtils.days(before, timeReference, roundCurrentTime), Locale.ShortFormat)
}
function monthStr(before = 0, timeReference = now, roundCurrentTime = true, shortFormat = true) {
const format = shortFormat ? "MMM" : "MMMM"
const timeStamp = LocaleUtils.months(before, timeReference, roundCurrentTime)
return LocaleUtils.formatDate(timeStamp, format)
}
function yearsStr(before = 0, timeReference = now, roundCurrentTime = true) {
return LocaleUtils.formatDate(LocaleUtils.years(before, timeReference, roundCurrentTime), "yyyy");
}
function hoursRangeStr(start, end) {
return "%1 - %2".arg(hourStr(0, start, false)).arg(hourStr(0, end, false))
}
function daysRangeStr(start, end) {
return (end - start > LocaleUtils.daysToMs(1)) ?
"%1 - %2".arg(dayStr(0, start, false)).arg(dayStr(0, end, false)) :
dayStr(0, start, false)
}
function monthsRangeStr(start, end) {
//End date excluded
//Adjust by one ms to exclude the end date
//To avoid considering the end date as a new month
end = end - 1
const startDate = monthStr(0, start, false)
const endDate = monthStr(0, end, false)
return (startDate !== endDate) ?
"%1 - %2".arg(startDate).arg(endDate) :
monthStr(0, start, false, false)
}
function yearsRangeStr(start, end) {
//End date excluded
//Adjust by one ms to exclude the end date
//To avoid considering the end date as a new year
end = end - 1
const startYear = yearsStr(0, start, false)
const endYear = yearsStr(0, end, false)
return (startYear !== endYear) ?
"%1 - %2".arg(startYear).arg(endYear) :
startYear
}
function getAdjustedTooltipPosition(event) {
// By defaullt the popup is displayed on the right of the cursor
// If there is not enough space on the right, display it on the left
const relativeMousePoint = event.target.mapToItem(toolTip.parent, event.x, event.y) // relative to tooltip parent
const leftPositon = (toolTip.parent.width - (toolTip.width + toolTip.rightPadding + relativeMousePoint.x + 15)) < 0
return leftPositon ? Qt.point(relativeMousePoint.x - toolTip.width - 15, relativeMousePoint.y - 5)
: Qt.point(relativeMousePoint.x + 15, relativeMousePoint.y - 5)
}
}
headerLeftPadding: 0
headerBottomPadding: Style.current.bigPadding
graphsModel: d.graphTabsModel
timeRangeModel: d.modelMetadata
onHeaderTabClicked: {
root.chart.animateToNewData();
}
/////////////////////////////
// Chartjs configuration //
/////////////////////////////
chart.chartType: 'bar'
chart.chartData: {
return {
labels: d.labels,
datasets: [{
xAxisId: 'x-axis-1',
yAxisId: 'y-axis-1',
backgroundColor: d.barColor,
pointRadius: 0,
hoverBackgroundColor: d.barColor,
hoverBorderColor: d.barBorderColor,
hoverBorderWidth: 2,
data: d.chartData
}]
}
}
chart.chartOptions: {
return {
maintainAspectRatio: false,
responsive: true,
legend: {
display: false
},
// Popup follows the cursor
onHover: function(arg1, hoveredItems, event) {
if(!event || hoveredItems.length == 0) {
toolTip.close()
return
}
d.hoveredBarIndex = hoveredItems[0]._index
d.hoveredBarValue = hoveredItems[0]._chart.config.data.datasets[0].data[hoveredItems[0]._index]
const position = d.getAdjustedTooltipPosition(event)
toolTip.popup(position.x, position.y)
},
tooltips: {
enabled: false,
},
scales: {
xAxes: [{
id: 'x-axis-1',
position: 'bottom',
stacked: false,
gridLines: {
drawOnChartArea: false,
drawBorder: false,
drawTicks: false,
},
ticks: {
fontSize: Style.current.asideTextFontSize,
fontColor: d.baseColor1,
padding: Style.current.padding,
}
}],
yAxes: [{
position: 'left',
id: 'y-axis-1',
stacked: false,
gridLines: {
borderDash: [5, 3],
lineWidth: 1,
drawBorder: false,
drawTicks: false,
color: d.twentyPercentBaseColor1,
},
beforeDataLimits: (axis) => {
axis.paddingTop = 25;
axis.paddingBottom = 0;
},
ticks: {
fontSize: 10,
fontColor: d.baseColor1,
padding: Style.current.halfPadding,
maxTicksLimit: Style.current.asideTextFontSize,
beginAtZero: true,
stepSize: 1,
callback: function(value, index, values) {
return LocaleUtils.numberToLocaleString(value)
}
}
}]
}
}
}
StatusMenu {
id: toolTip
width: 243 //By design
topPadding: Style.current.padding
bottomPadding: topPadding
leftPadding: topPadding
rightPadding: topPadding
parent: Overlay.overlay
ColumnLayout {
spacing: Style.current.padding
RowLayout {
Layout.fillWidth: true
StatusBaseText {
elide: Qt.ElideRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1
text: d.tooltipConfig.timeRangeString
}
Item {
Layout.fillWidth: true
}
StatusBaseText {
Layout.alignment: Qt.AlignRight
elide: Qt.ElideRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.directColor1
text: d.hoveredModelMetadata ? d.tooltipConfig.timeRangeFormatter(d.hoveredModelMetadata.start, d.hoveredModelMetadata.end)
: ""
}
}
RowLayout {
Layout.fillWidth: true
StatusBaseText {
elide: Qt.ElideRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1
text: qsTr("No. of Messages")
}
Item { Layout.fillWidth: true }
StatusBaseText {
Layout.alignment: Qt.AlignRight
elide: Qt.ElideRight
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.directColor1
text: LocaleUtils.numberToLocaleString(d.hoveredBarValue)
}
}
}
}
}

View File

@ -55,7 +55,7 @@ StackLayout {
SettingsPage { SettingsPage {
rightPadding: 64 rightPadding: 64
bottomPadding: 64 bottomPadding: 50
topPadding: 0 topPadding: 0
header: null header: null
contentItem: ColumnLayout { contentItem: ColumnLayout {
@ -109,38 +109,28 @@ StackLayout {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 1 implicitHeight: 1
visible: root.editable
color: Theme.palette.statusMenu.separatorColor color: Theme.palette.statusMenu.separatorColor
} }
RowLayout { OverviewSettingsChart {
Layout.topMargin: 16
Layout.fillWidth: true Layout.fillWidth: true
visible: root.owned
StatusIcon {
icon: "info"
color: Theme.palette.directColor1
}
StatusBaseText {
Layout.fillWidth: true
text: qsTr("This node is the Community Owner Node. For your Community to function correctly try to keep this computer with Status running and online as much as possible.")
font.pixelSize: 15
color: Theme.palette.directColor1
wrapMode: Text.WordWrap
}
}
Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.bottomMargin: 16
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: Theme.palette.statusMenu.separatorColor
} }
} }
footer: OverviewSettingsFooter { footer: OverviewSettingsFooter {
rightPadding: 64 rightPadding: 64
leftPadding: 64 leftPadding: 64
bottomPadding: 50 bottomPadding: 64
topPadding: 0
loginType: root.loginType loginType: root.loginType
communityName: root.name communityName: root.name
//TODO connect to backend //TODO connect to backend

View File

@ -31,3 +31,4 @@ TokenHoldersProxyModel 1.0 TokenHoldersProxyModel.qml
WarningPanel 1.0 WarningPanel.qml WarningPanel 1.0 WarningPanel.qml
WelcomeBannerPanel 1.0 WelcomeBannerPanel.qml WelcomeBannerPanel 1.0 WelcomeBannerPanel.qml
EditSettingsPanel 1.0 EditSettingsPanel.qml EditSettingsPanel 1.0 EditSettingsPanel.qml
OverviewSettingsChart 1.0 OverviewSettingsChart.qml