diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/AnalyticsChart.client.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/AnalyticsChart.client.vue index 712a8fe9b..2c3afa763 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/AnalyticsChart.client.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/AnalyticsChart.client.vue @@ -27,6 +27,7 @@ import { LineController, LineElement, PointElement, + type ScriptableLineSegmentContext, Tooltip, } from 'chart.js' @@ -446,6 +447,12 @@ function buildDatasets() { pointHoverBackgroundColor: colors.borderColor, pointHoverBorderWidth: 0, pointHitRadius: 16, + segment: dataset.lastDataPointUnavailable + ? { + borderColor: (context: ScriptableLineSegmentContext) => + context.p1DataIndex === dataset.data.length - 1 ? 'transparent' : undefined, + } + : undefined, stack: props.stacked ? 'analytics' : undefined, } }) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue index 109eab32e..c36913638 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue @@ -59,7 +59,9 @@ {{ formatMessage(analyticsChartMessages.total) }} - {{ formattedTotal }} + + {{ formattedTotal }} +
{{ entry.formattedValue }} @@ -148,6 +154,7 @@ export type AnalyticsChartTooltipEntry = { tooltip?: string color: string formattedValue: string + noData: boolean hidden: boolean toggleDisabled: boolean isPreviousPeriod?: boolean @@ -164,6 +171,7 @@ const props = defineProps<{ chartStart: Date | null chartEnd: Date | null formattedTotal: string + totalHasData: boolean entries: AnalyticsChartTooltipEntry[] containerWidth: number containerHeight: number diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue index 91fe11d48..0d19d9aa7 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue @@ -64,6 +64,7 @@ :chart-start="chartRangeBounds?.start ?? null" :chart-end="chartRangeBounds?.end ?? null" :formatted-total="hoverFormattedTotal" + :total-has-data="hoverHasData" :entries="hoverEntries" :container-width="containerSize.width" :container-height="containerSize.height" @@ -89,6 +90,7 @@ import type { AnalyticsGroupByPreset, } from '~/providers/analytics/analytics' +import { analyticsChartMessages } from '../../analytics-messages.ts' import type { AnalyticsChartLegendEntry, AnalyticsChartRangeBounds, @@ -188,7 +190,26 @@ const hoverTotalValue = computed(() => { }, 0) }) +function hasDatasetDataAtSlice(dataset: ChartDataset | undefined, sliceIndex: number) { + if (!dataset) return false + return !dataset.lastDataPointUnavailable || sliceIndex !== dataset.data.length - 1 +} + +const hoverHasData = computed(() => { + if (hoverState.sliceIndex === null) return false + const sliceIndex = hoverState.sliceIndex + + return props.currentLegendEntries.some((legendEntry) => { + if (legendEntry.hidden) return false + const dataset = props.chartDatasetById.get(legendEntry.id) + return hasDatasetDataAtSlice(dataset, sliceIndex) + }) +}) + const hoverFormattedTotal = computed(() => { + if (!hoverHasData.value) { + return formatMessage(analyticsChartMessages.noData) + } if (props.isRatioMode) { return hoverTotalValue.value > 0 ? '100%' : '0%' } @@ -203,6 +224,7 @@ const hoverEntries = computed(() => { return props.legendEntries.map((legendEntry) => { const dataset = props.chartDatasetById.get(legendEntry.id) const value = dataset?.data[sliceIndex] ?? 0 + const hasData = hasDatasetDataAtSlice(dataset, sliceIndex) const ratioValue = legendEntry.hidden || totalValue === 0 ? 0 : (value / totalValue) * 100 return { projectId: legendEntry.id, @@ -210,9 +232,12 @@ const hoverEntries = computed(() => { projectName: legendEntry.projectName, tooltip: legendEntry.tooltip, color: legendEntry.color, - formattedValue: props.isRatioMode - ? `${ratioValue.toFixed(1)}%` - : formatMetricValue(value, props.activeStat, formatNumber, formatMessage), + formattedValue: hasData + ? props.isRatioMode + ? `${ratioValue.toFixed(1)}%` + : formatMetricValue(value, props.activeStat, formatNumber, formatMessage) + : formatMessage(analyticsChartMessages.noData), + noData: !hasData, hidden: legendEntry.hidden, toggleDisabled: !legendEntry.hidden && isLegendEntryToggleDisabled(legendEntry), isPreviousPeriod: legendEntry.isPreviousPeriod, diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts index 84144f344..e09e5b615 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts @@ -38,6 +38,7 @@ export type ChartDataset = { projectName?: string tooltip?: string data: number[] + lastDataPointUnavailable?: boolean borderColor: string backgroundColor: string borderDash?: number[] diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/use-analytics-chart-datasets.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/use-analytics-chart-datasets.ts index 482c87900..2b0c92de8 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/use-analytics-chart-datasets.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/use-analytics-chart-datasets.ts @@ -32,6 +32,7 @@ import { type ChartDataset, getChartDatasetTotal, getShortHourlyAxisTickLimit, + getSliceBucketRange, getSliceCount, shouldCapitalizeBreakdownLabel, } from './analytics-chart-utils' @@ -161,8 +162,8 @@ export function useAnalyticsChartDatasets( ) : undefined }) - const chartDatasetsByStat = computed>(() => - buildDatasetsByStat( + const chartDatasetsByStat = computed>(() => { + const datasets = buildDatasetsByStat( context.displayedTimeSlices.value, selectedProjects.value, legendPalette.value, @@ -175,8 +176,20 @@ export function useAnalyticsChartDatasets( showProjectVersionNames.value ? context.getVersionProjectName : undefined, formatMessage, sliceCount.value, - ), - ) + ) + const nextFetchRequest = context.displayedFetchRequest.value + if ( + nextFetchRequest && + context.displayedSelectedGroupBy.value === 'day' && + isLastBucketCurrentDay(nextFetchRequest.time_range, sliceCount.value) + ) { + datasets.revenue = datasets.revenue.map((dataset) => ({ + ...dataset, + lastDataPointUnavailable: true, + })) + } + return datasets + }) const previousChartDatasetsByStat = computed>(() => buildDatasetsByStat( context.displayedPreviousTimeSlices.value, @@ -406,6 +419,16 @@ function buildDatasetsByStat( return datasetsByStat } +function isLastBucketCurrentDay(timeRange: Labrinth.Analytics.v3.TimeRange, sliceCount: number) { + const lastBucket = getSliceBucketRange(timeRange, sliceCount, sliceCount - 1) + const todayStart = new Date() + todayStart.setHours(0, 0, 0, 0) + const tomorrowStart = new Date(todayStart) + tomorrowStart.setDate(tomorrowStart.getDate() + 1) + + return lastBucket.start < tomorrowStart && lastBucket.end > todayStart +} + function sortDatasetsByTotal(datasets: ChartDataset[]) { return [...datasets] .sort((a, b) => { diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts index f388bd3df..d669769ec 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts @@ -688,6 +688,10 @@ export const analyticsChartMessages = defineMessages({ id: 'analytics.chart.tooltip.hide-entry', defaultMessage: 'Hide {name} in graph', }, + noData: { + id: 'analytics.chart.tooltip.no-data', + defaultMessage: 'No data', + }, durationDays: { id: 'analytics.chart.tooltip.duration.days', defaultMessage: '{count, plural, one {# day} other {# days}}', diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json index 1c7cdbd4e..aac072810 100644 --- a/apps/frontend/src/locales/en-US/index.json +++ b/apps/frontend/src/locales/en-US/index.json @@ -191,6 +191,9 @@ "analytics.chart.tooltip.hide-entry": { "message": "Hide {name} in graph" }, + "analytics.chart.tooltip.no-data": { + "message": "No data" + }, "analytics.chart.tooltip.pinned": { "message": "Chart tooltip pinned" },