From c89e80f34d5fd1b00e26a8ebf740a4db018fbfbe Mon Sep 17 00:00:00 2001 From: Aleksandr Gorodetskii Date: Wed, 1 Mar 2023 19:15:57 +0300 Subject: [PATCH] GUI Billing enhancements (issue #3080): layers chart - fix dataLabels placement. Highlight selected tick. Tooltips name mapping. Total dataLabel --- .../extensions/barchart-data-label-plugin.js | 63 +++++++++++++++---- .../extensions/highlight-ticks-plugin.js | 4 +- .../billing/reports/charts/storage-layers.js | 18 ++++-- .../billing/reports/storage-report.js | 61 ++++++++++++------ .../models/billing/base-billing-request.js | 25 +++++++- client/src/models/billing/index.js | 5 +- 6 files changed, 134 insertions(+), 42 deletions(-) diff --git a/client/src/components/billing/reports/charts/extensions/barchart-data-label-plugin.js b/client/src/components/billing/reports/charts/extensions/barchart-data-label-plugin.js index 62d139164c..609fb6a0cb 100644 --- a/client/src/components/billing/reports/charts/extensions/barchart-data-label-plugin.js +++ b/client/src/components/billing/reports/charts/extensions/barchart-data-label-plugin.js @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 EPAM Systems, Inc. (https://www.epam.com/) + * Copyright 2017-2023 EPAM Systems, Inc. (https://www.epam.com/) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ function isNotSet (v) { const plugin = { id, afterDatasetsDraw: function (chart, ease, pluginOptions) { - const {valueFormatter = costTickFormatter} = pluginOptions || {}; + const {valueFormatter = costTickFormatter, chartType} = pluginOptions || {}; const ctx = chart.chart.ctx; const datasetLabels = chart.data.datasets .map((dataset, i) => { @@ -39,7 +39,8 @@ const plugin = { index, meta, chart, - valueFormatter + valueFormatter, + chartType )); } return []; @@ -135,7 +136,8 @@ const plugin = { borderColor = 'black', textBold = false, textColor, - flagColor + flagColor, + dataLabelText } = dataset; const color = textColor || borderColor; const {position, text} = config; @@ -145,8 +147,11 @@ const plugin = { ctx.font = `${textBold ? 'bold ' : ''}9pt sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; - ctx.fillText(text, position.x + position.width / 2.0, position.y + position.height); - if (flagColor) { + ctx.fillText( + `${dataLabelText || ''}${text}`, + position.x + position.width / 2.0, position.y + position.height + ); + if (flagColor && !dataLabelText) { ctx.beginPath(); ctx.arc( position.x, @@ -162,14 +167,23 @@ const plugin = { } } }, - getInitialLabelConfig: function (dataset, element, index, meta, chart, valueFormatter) { + getInitialLabelConfig: function ( + dataset, + element, + index, + meta, + chart, + valueFormatter, + chartType + ) { const { data, hidden, - textBold = false + textBold = false, + showDataLabel = false } = dataset; const {xAxisID, yAxisID} = meta; - if (hidden) { + if (hidden && !showDataLabel) { return null; } const xAxis = chart.scales[xAxisID]; @@ -185,15 +199,38 @@ const plugin = { left: xAxis.left, right: xAxis.right }; + const getLabelXY = () => { + const visibleOnlyLabel = hidden && showDataLabel; + let currentLabelY = yAxis.getPixelForValue(dataItem); + if (visibleOnlyLabel) { + const padding = (globalBounds.bottom - globalBounds.top) * 0.1; + currentLabelY = yAxis.getPixelForValue(data[index]) - padding; + } + let offset = 0; + if (chartType === 'stacked') { + const datasetIndex = element._datasetIndex; + if (datasetIndex > 0 && !visibleOnlyLabel) { + for (let i = 0; i < datasetIndex; i++) { + const dataset = chart.data.datasets[i]; + const prevData = (dataset || {}).data || []; + const prevLabelHeight = globalBounds.bottom - yAxis + .getPixelForValue(prevData[index] || 0); + offset += prevLabelHeight; + } + } + } + return { + x: element.getCenterPoint().x, + y: currentLabelY - offset + }; + }; const labelText = valueFormatter(dataItem); ctx.font = `${textBold ? 'bold ' : ''}9pt sans-serif`; const {width: labelWidth} = ctx.measureText(labelText); const padding = {x: 5, y: 2}; const margin = 3; const labelHeight = 10; - const {x} = element.getCenterPoint(); - const y = yAxis.getPixelForValue(dataItem); - const dataPoint = {x, y}; + const {x, y} = getLabelXY(); const labelTotalWidth = labelWidth + 2.0 * (margin + padding.x); const labelTotalHeight = labelHeight + 2.0 * (margin + padding.y); const getLabelPosition = (yy = y) => { @@ -208,7 +245,7 @@ const plugin = { dataset, globalBounds, label: { - dataPoint, + dataPoint: {x, y}, getLabelPosition, labelHeight: labelTotalHeight, text: labelText diff --git a/client/src/components/billing/reports/charts/extensions/highlight-ticks-plugin.js b/client/src/components/billing/reports/charts/extensions/highlight-ticks-plugin.js index 2fa8859e6d..6048ad808b 100644 --- a/client/src/components/billing/reports/charts/extensions/highlight-ticks-plugin.js +++ b/client/src/components/billing/reports/charts/extensions/highlight-ticks-plugin.js @@ -30,7 +30,9 @@ const plugin = { const ticks = ((chart.scales || {})[axis] || {})._ticks; if (ticks && ticks.length) { for (const tick of ticks) { - tick.major = highlightTickFn(request.value[tick.value]); + const storage = (request || {}).value; + const value = (storage || {})[tick.value]; + tick.major = highlightTickFn(value, tick); } } } diff --git a/client/src/components/billing/reports/charts/storage-layers.js b/client/src/components/billing/reports/charts/storage-layers.js index 50fbdfc036..1e7dd9262c 100644 --- a/client/src/components/billing/reports/charts/storage-layers.js +++ b/client/src/components/billing/reports/charts/storage-layers.js @@ -19,7 +19,8 @@ import {inject, observer} from 'mobx-react'; import Chart from './base'; import { BarchartDataLabelPlugin, - ChartClickPlugin + ChartClickPlugin, + HighlightTicksPlugin } from './extensions'; import Export from '../export'; import {costTickFormatter} from '../utilities'; @@ -82,7 +83,9 @@ function StorageLayers ( borderWidth: 2, borderDash: [4, 4], borderColor: baseColors, - backgroundColor: backgroundColors + backgroundColor: backgroundColors, + flagColor: baseColors[index], + textColor: reportThemes.textColor }; }) }; @@ -167,9 +170,13 @@ function StorageLayers ( } }, plugins: { - // todo: improve labels Y-align, according to stacked chart type + [HighlightTicksPlugin.id]: { + highlightTickFn, + axis: 'x-axis' + }, [BarchartDataLabelPlugin.id]: { - valueFormatter + valueFormatter, + chartType: 'stacked' }, [ChartClickPlugin.id]: { handler: onSelect ? index => onSelect({key: aggregates[index]}) : undefined, @@ -220,7 +227,8 @@ function StorageLayers ( options={options} plugins={[ BarchartDataLabelPlugin.plugin, - ChartClickPlugin.plugin + ChartClickPlugin.plugin, + HighlightTicksPlugin.plugin ]} useChartImageGenerator={useImageConsumer} onImageDataReceived={onImageDataReceived} diff --git a/client/src/components/billing/reports/storage-report.js b/client/src/components/billing/reports/storage-report.js index 9038fd6469..a34d4bf8cc 100644 --- a/client/src/components/billing/reports/storage-report.js +++ b/client/src/components/billing/reports/storage-report.js @@ -17,8 +17,7 @@ import React from 'react'; import {inject, observer} from 'mobx-react'; import { - Pagination, - Table + Pagination } from 'antd'; import moment from 'moment-timezone'; import {computed} from 'mobx'; @@ -54,7 +53,8 @@ import { GetGroupedObjectStorages, GetGroupedObjectStoragesWithPrevious, GetObjectStorageLayersInfo, - preFetchBillingRequest + preFetchBillingRequest, + LAYERS_KEYS } from '../../../models/billing'; import {StorageReportLayout, Layout} from './layout'; import { @@ -70,6 +70,13 @@ import styles from './storage-report.css'; const tablePageSize = 10; +const LAYERS_LABELS = { + [LAYERS_KEYS.avgSize]: 'Average size', + [LAYERS_KEYS.oldVersionAvgSize]: 'Old versions average size', + [LAYERS_KEYS.cost]: 'Cost', + [LAYERS_KEYS.oldVersionCost]: 'Old versions cost' +}; + function injection (stores, props) { const {location, params} = props; const {type} = params || {}; @@ -192,9 +199,9 @@ function renderTable ( } = item || {}; if (!showDetails) { switch (key) { - case 'size': return (usageLast || 0); - case 'avgSize': return (usage || 0); - case 'cost': return (value || 0); + case LAYERS_KEYS.size: return (usageLast || 0); + case LAYERS_KEYS.avgSize: return (usage || 0); + case LAYERS_KEYS.cost: return (value || 0); default: return 0; } @@ -300,22 +307,22 @@ function renderTable ( ...getDetailedCells({ title: 'Cost', dataExtractor: getLayerCostValue, - currentKey: 'cost', - oldVersionsKey: 'oldVersionCost' + currentKey: LAYERS_KEYS.cost, + oldVersionsKey: LAYERS_KEYS.oldVersionCost }), ...getDetailedCells({ title: 'Avg. Vol.', measure: 'GB', dataExtractor: getLayerSizeValue, - currentKey: 'avgSize', - oldVersionsKey: 'oldVersionAvgSize' + currentKey: LAYERS_KEYS.avgSize, + oldVersionsKey: LAYERS_KEYS.oldVersionAvgSize }), ...getDetailedCells({ title: 'Cur. Vol.', measure: 'GB', dataExtractor: getLayerSizeValue, - currentKey: 'size', - oldVersionsKey: 'oldVersionSize' + currentKey: LAYERS_KEYS.size, + oldVersionsKey: LAYERS_KEYS.oldVersionSize }), { key: 'region', @@ -574,20 +581,31 @@ class StorageReports extends React.Component { }); return result; }; + const filter = metrics === StorageMetrics.volume - ? ['avgSize', 'oldVersionAvgSize'] - : ['cost', 'oldVersionCost']; + ? [LAYERS_KEYS.avgSize, LAYERS_KEYS.oldVersionAvgSize] + : [LAYERS_KEYS.cost, LAYERS_KEYS.oldVersionCost]; const datasets = filter .map(key => { return { - label: key, + label: LAYERS_LABELS[key] || key, data: getData(key, labels) }; }); + const totalDataset = (datasets || []).reduce((acc, current) => { + acc.data = current.data.map((value, index) => value + (acc.data[index] || 0)); + return acc; + }, { + data: [], + label: 'Total', + dataLabelText: 'Total: ', + hidden: true, + showDataLabel: true + }); return { aggregates, labels: labels.map(getStorageClassName), - datasets + datasets: [...datasets, totalDataset] }; } @@ -621,6 +639,7 @@ class StorageReports extends React.Component { ? 'by volume' : undefined; const showTableDetails = /^object$/i.test(type); + const selectedIndex = tiersData.aggregates.indexOf(storageAggregate); return ( { @@ -692,15 +711,19 @@ class StorageReports extends React.Component { tick._index === selectedIndex + } + highlightTickStyle={{ + fontColor: reportThemes.current + }} /> ) diff --git a/client/src/models/billing/base-billing-request.js b/client/src/models/billing/base-billing-request.js index 23e20cd774..ed8fcc4062 100644 --- a/client/src/models/billing/base-billing-request.js +++ b/client/src/models/billing/base-billing-request.js @@ -4,6 +4,17 @@ import TimedOutCache from '../basic/timed-out-cache'; import {bytesToGbs, costMapper} from './utils'; import defer from '../../utils/defer'; +export const KEYS = { + cost: 'cost', + oldVersionCost: 'oldVersionCost', + accumulativeCost: 'accumulativeCost', + accumulativeOldVersionCost: 'accumulativeOldVersionCost', + size: 'size', + avgSize: 'avgSize', + oldVersionSize: 'oldVersionSize', + oldVersionAvgSize: 'oldVersionAvgSize' +}; + /** * @typedef {Object} BaseBillingRequestPagination * @property {number} pageNum @@ -139,8 +150,18 @@ export default class BaseBillingRequest extends RemotePost { postprocess (value) { const items = super.postprocess(value); - const costKeys = ['cost', 'oldVersionCost', 'accumulativeCost', 'accumulativeOldVersionCost']; - const sizeKeys = ['size', 'avgSize', 'oldVersionSize', 'oldVersionAvgSize']; + const costKeys = [ + KEYS.cost, + KEYS.oldVersionCost, + KEYS.accumulativeCost, + KEYS.accumulativeOldVersionCost + ]; + const sizeKeys = [ + KEYS.size, + KEYS.avgSize, + KEYS.oldVersionSize, + KEYS.oldVersionAvgSize + ]; const processTier = (tier) => { const result = { ...tier diff --git a/client/src/models/billing/index.js b/client/src/models/billing/index.js index dab5e3fa28..a1454a77c0 100644 --- a/client/src/models/billing/index.js +++ b/client/src/models/billing/index.js @@ -35,7 +35,7 @@ import {GetGroupedPipelines, GetGroupedPipelinesWithPrevious} from './get-groupe import {GetGroupedTools, GetGroupedToolsWithPrevious} from './get-grouped-tools-data'; import {GetGroupedUsers} from './get-grouped-users'; import GetObjectStorageLayersInfo from './get-object-storage-layers-info'; -import {preFetchBillingRequest} from './base-billing-request'; +import {preFetchBillingRequest, KEYS as LAYERS_KEYS} from './base-billing-request'; export { FetchBillingCenters, @@ -58,5 +58,6 @@ export { GetGroupedTools, GetGroupedToolsWithPrevious, GetObjectStorageLayersInfo, - preFetchBillingRequest + preFetchBillingRequest, + LAYERS_KEYS };