Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(analytics-chart): style null / empty entities [MA-3510] #1886

Merged
merged 2 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
data-testid="legend"
>
<li
v-for="{ fillStyle, strokeStyle, text, datasetIndex, index, value } in (items as any[])"
v-for="{ fillStyle, strokeStyle, text, datasetIndex, index, value, isSegmentEmpty } in (items as any[])"
:key="text"
ref="legendItemsRef"
@click="handleLegendItemClick(datasetIndex, index)"
Expand All @@ -22,7 +22,7 @@
>
<div
class="label"
:class="{ 'truncate-label' : shouldTruncate }"
:class="{ 'truncate-label' : shouldTruncate, empty: isSegmentEmpty }"
:title="text"
>
{{ text }}
Expand Down Expand Up @@ -305,6 +305,7 @@ const positionToClass = (position: `${ChartLegendPosition}`) => {
margin-top: 0;
.label {
line-height: $kui-line-height-40;
padding-right: $kui-space-10; // Ensure italics text doesn't get cut off.
white-space: nowrap;
}
}
Expand Down Expand Up @@ -337,6 +338,10 @@ const positionToClass = (position: `${ChartLegendPosition}`) => {
.strike-through {
text-decoration: line-through;
}

.empty {
font-style: italic;
}
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@
class="tooltip"
>
<template
v-for="{ backgroundColor, borderColor, label, value } in (series as any)"
v-for="{ backgroundColor, borderColor, label, value, isSegmentEmpty } in (series as any)"
:key="label"
>
<li v-if="series.length">
<div
class="square-marker"
:style="{ background: backgroundColor, 'border-color': borderColor }"
/>
<span class="display-label">{{ label }}</span>
<span
class="display-label"
:class="{ empty: isSegmentEmpty }"
>{{ label }}</span>
<span class="display-value">{{ value }}</span>
</li>
</template>
Expand Down Expand Up @@ -257,9 +260,9 @@ ul.tooltip {
}

li {
align-items: center;
display: flex;
font-size: var(--kui-font-size-30, $kui-font-size-30);
line-height: 1;
margin: var(--kui-space-40, $kui-space-40);
}

Expand All @@ -268,10 +271,15 @@ ul.tooltip {
}

.display-label {
flex: 1;
max-width: 75%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

&.empty {
font-style: italic;
}
}

.display-value {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@
</template>

<script setup lang="ts">
import type { ChartDataset, ChartOptions } from 'chart.js'
import type { ChartDataset, ChartOptions, LegendItem } from 'chart.js'
import { Chart } from 'chart.js'
import type { EventContext } from 'chartjs-plugin-annotation'
import annotationPlugin from 'chartjs-plugin-annotation'
import { ref, toRef, onMounted, computed, reactive, watch, inject, onBeforeUnmount, onUnmounted } from 'vue'
import type { PropType, Ref } from 'vue'
import ToolTip from '../chart-plugins/ChartTooltip.vue'
import ChartLegend from '../chart-plugins/ChartLegend.vue'
import type { BarChartData } from '../../utils'
import { type BarChartData, generateLegendItems } from '../../utils'
import { accessibleGrey, MAX_LABEL_LENGTH, formatNumber, getTextHeight, getTextWidth, drawPercentage, dataTotal, conditionalDataTotal, debounce } from '../../utils'
import composables from '../../composables'
import { v4 as uuidv4 } from 'uuid'
Expand Down Expand Up @@ -206,7 +206,7 @@ const axis = ref< HTMLCanvasElement>()
const legendID = ref(uuidv4())
const reactiveAnnotationsID = uuidv4()
const maxOverflowPluginID = uuidv4()
const legendItems = ref([])
const legendItems = ref<LegendItem[]>([])
const legendPosition = ref(inject('legendPosition', ChartLegendPosition.Right))
const axesTooltip = ref<AxesTooltipState>({
show: false,
Expand Down Expand Up @@ -248,10 +248,8 @@ const htmlLegendPlugin = {
// Update any computed properties that depend on chart state.
// As of writing, this is important for correctly calculating maxOverflow based on dataset visibility.
dependsOnChartUpdate.value += 1
// @ts-ignore - ChartJS types are incomplete
legendItems.value = chart.options.plugins.legend.labels.generateLabels(chart)
.map(e => ({ ...e, value: props.legendValues && props.legendValues[e.text] }))
.sort(props.chartLegendSortFn)

legendItems.value = generateLegendItems(chart, props.legendValues, props.chartLegendSortFn)
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import type { ChartLegendSortFn, ChartTooltipSortFn, EnhancedLegendItem, KChartD
import type { GranularityValues, AbsoluteTimeRangeV4 } from '@kong-ui-public/analytics-utilities'
import type { Chart, LegendItem } from 'chart.js'
import { ChartLegendPosition } from '../../enums'
import { formatByGranularity } from '../../utils'
import { formatByGranularity, generateLegendItems } from '../../utils'

const props = defineProps({
chartData: {
Expand Down Expand Up @@ -185,11 +185,7 @@ const tooltipData = reactive({
const htmlLegendPlugin = {
id: legendID.value,
afterUpdate(chart: Chart) {
// @ts-ignore - ChartJS types are incomplete
legendItems.value = chart.options.plugins.legend.labels.generateLabels(chart)
.map(e => ({ ...e, value: props.legendValues && props.legendValues[e.text] }))
.filter(e => !e.value.isThreshold)
.sort(props.chartLegendSortFn)
legendItems.value = generateLegendItems(chart, props.legendValues, props.chartLegendSortFn)
},
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BarChartOptions, ExternalTooltipContext } from '../types'
import type { BarChartOptions, ExternalTooltipContext, KChartData } from '../types'
import { Tooltip, Interaction } from 'chart.js'
import type {
TooltipPositionerFunction,
Expand All @@ -8,6 +8,7 @@ import type {
InteractionModeFunction,
InteractionItem,
CategoryScale,
Chart,
} from 'chart.js'
import { isNullOrUndef, getRelativePosition } from 'chart.js/helpers'
import { FONT_SIZE_SMALL, FONT_SIZE_REGULAR, MAX_LABEL_LENGTH, horizontalTooltipPositioning, tooltipBehavior } from '../utils'
Expand Down Expand Up @@ -76,6 +77,28 @@ export default function useBarChartOptions(chartOptions: BarChartOptions) {
// For larger datasets, allow enough vertical space to display all axis labels
const labelFontSize = chartOptions.numLabels.value > 25 ? FONT_SIZE_SMALL : FONT_SIZE_REGULAR

const genTickFont = ({ chart, index }: { chart: Chart, index: number }) => {
const data = chart.data as KChartData | undefined
return {
size: labelFontSize,
style: data?.isLabelEmpty?.[index] ? 'italic' : 'normal',
}
}

const genTickLabel = (scale: CategoryScale, value: string, index: number) => {
if (scale.chart.options.indexAxis === scale.axis) {
value = scale.getLabelForValue(index)

if (value && value.length > MAX_LABEL_LENGTH) {
return value.slice(0, MAX_LABEL_LENGTH) + '...'
} else {
return value
}
}

return scale.getLabelForValue(Number(value))
}

const options = computed(() => {
return {
indexAxis: chartOptions.indexAxis,
Expand All @@ -93,20 +116,10 @@ export default function useBarChartOptions(chartOptions: BarChartOptions) {
ticks: {
maxRotation: 90,
autoSkip: false,
font: labelFontSize,
font: genTickFont,
callback: function(value: string, index: number): string {
const that = this as unknown as CategoryScale
if (that.chart.options.indexAxis === that.axis) {
value = that.getLabelForValue(index)
const maxLabelLength = 10
if (value && value.length > maxLabelLength) {
return value.slice(0, maxLabelLength) + '...'
} else {
return value
}
}

return that.getLabelForValue(Number(value))
return genTickLabel(that, value, index)
},
},
title: {
Expand All @@ -133,21 +146,10 @@ export default function useBarChartOptions(chartOptions: BarChartOptions) {
drawBorder: false,
},
ticks: {
font: labelFontSize,
font: genTickFont,
callback: function(value: string, index: number): string {
const that = this as unknown as CategoryScale

if (that.chart.options.indexAxis === that.axis) {
value = that.getLabelForValue(index)

if (value && value.length > MAX_LABEL_LENGTH) {
return value.slice(0, MAX_LABEL_LENGTH) + '...'
} else {
return value
}
}

return that.getLabelForValue(Number(value))
return genTickLabel(that, value, index)
},
},
title: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ describe('useVitalsExploreDatasets', () => {
null,
],
label: 'dimension',
isSegmentEmpty: false,
},
],
labels: [
'dimension',
],
isLabelEmpty: [false],
})
})

Expand Down Expand Up @@ -106,8 +108,9 @@ describe('useVitalsExploreDatasets', () => {
{
labels: ['dimension1'],
datasets: [
{ label: 'dimension1', backgroundColor: '#a86cd5', data: [1] },
{ label: 'dimension1', backgroundColor: '#a86cd5', data: [1], isSegmentEmpty: false },
],
isLabelEmpty: [false],
},
)
})
Expand Down Expand Up @@ -169,11 +172,79 @@ describe('useVitalsExploreDatasets', () => {
{
labels: ['GroupBy2', 'GroupBy1'],
datasets: [
{ label: 'ThenBy1', backgroundColor: '#a86cd5', data: [null, 100] },
{ label: 'ThenBy2', backgroundColor: '#6a86d2', data: [null, 150] },
{ label: 'ThenBy3', backgroundColor: '#00bbf9', data: [200, null] },
{ label: 'ThenBy4', backgroundColor: '#00c4b0', data: [250, null] },
{ label: 'ThenBy1', backgroundColor: '#a86cd5', data: [null, 100], isSegmentEmpty: false },
{ label: 'ThenBy2', backgroundColor: '#6a86d2', data: [null, 150], isSegmentEmpty: false },
{ label: 'ThenBy3', backgroundColor: '#00bbf9', data: [200, null], isSegmentEmpty: false },
{ label: 'ThenBy4', backgroundColor: '#00c4b0', data: [250, null], isSegmentEmpty: false },
],
isLabelEmpty: [false, false],
},
)
})

it('handles empty by looking at ID', () => {
const exploreResult: ComputedRef<ExploreResultV4> = computed(() => ({
data: [
{
timestamp: '2023-02-20T21:00:00.000Z',
event: {
GroupBy: 'empty',
ThenBy: 'then-by-1',
request_count: 100,
},
},
{
timestamp: '2023-02-20T21:00:00.000Z',
event: {
GroupBy: 'empty',
ThenBy: 'empty',
request_count: 150,
},
},
{
timestamp: '2023-02-20T21:00:00.000Z',
event: {
GroupBy: 'group-by-2',
ThenBy: 'then-by-3',
request_count: 200,
},
},
{
timestamp: '2023-02-20T21:00:00.000Z',
event: {
GroupBy: 'group-by-2',
ThenBy: 'then-by-4',
request_count: 250,
},
},
],
meta: {
start_ms: 1669928400000,
end_ms: 1670014800000,
granularity_ms: 86400000,
metric_names: ['request_count'],
display: {
GroupBy: { 'empty': { name: 'GroupBy1' }, 'group-by-2': { name: 'GroupBy2' } },
ThenBy: { 'then-by-1': { name: 'ThenBy1' }, 'empty': { name: 'ThenBy2' }, 'then-by-3': { name: 'ThenBy3' }, 'then-by-4': { name: 'ThenBy4' } },
},
query_id: '',
metric_units: { request_count: 'units' },
truncated: false,
limit: 15,
} as QueryResponseMeta,
}))
const result = useExploreResultToDatasets({ fill: true }, exploreResult)

expect(result.value).toEqual(
{
labels: ['GroupBy2', 'GroupBy1'],
datasets: [
{ label: 'ThenBy1', backgroundColor: '#a86cd5', data: [null, 100], isSegmentEmpty: false },
{ label: 'ThenBy3', backgroundColor: '#6a86d2', data: [200, null], isSegmentEmpty: false },
{ label: 'ThenBy4', backgroundColor: '#00bbf9', data: [250, null], isSegmentEmpty: false },
{ label: 'ThenBy2', backgroundColor: '#afb7c5', data: [null, 150], isSegmentEmpty: true },
],
isLabelEmpty: [false, true],
},
)
})
Expand Down Expand Up @@ -206,8 +277,9 @@ describe('useVitalsExploreDatasets', () => {
{
labels: ['Request Count'],
datasets: [
{ label: 'Request Count', backgroundColor: '#a86cd5', data: [1] },
{ label: 'Request Count', backgroundColor: '#a86cd5', data: [1], isSegmentEmpty: false },
],
isLabelEmpty: [false],
},
)
})
Expand Down Expand Up @@ -247,6 +319,7 @@ describe('useVitalsExploreDatasets', () => {
{ label: 'metric1', backgroundColor: '#a86cd5', data: [1, null] },
{ label: 'metric2', backgroundColor: '#6a86d2', data: [null, 2] },
],
isLabelEmpty: [false, false],
},
)
})
Expand Down Expand Up @@ -300,6 +373,7 @@ describe('useVitalsExploreDatasets', () => {
{ label: 'metric1', backgroundColor: '#a86cd5', data: [3, 1] },
{ label: 'metric2', backgroundColor: '#6a86d2', data: [4, 2] },
],
isLabelEmpty: [false, false],
},
)
})
Expand Down
Loading
Loading