Skip to content

Commit

Permalink
fix: adjust column width to prevent legend items from overlapping (#795)
Browse files Browse the repository at this point in the history
- set the grid-column-template width based on the widest item
  - implement a grid formatting function in order to better handle
    dynamic sized legend items. Pure CSS was not enough to handle
    both short as well as long legend item values.
  - To avoid unecesarry calls to a potentially DOM-modifying function
    only execute formatGrid() if the legend items have changed.
  • Loading branch information
filipgutica authored Sep 20, 2023
1 parent 1c1a4ad commit dbef4ad
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 104 deletions.
2 changes: 1 addition & 1 deletion packages/analytics/analytics-chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ yarn add @kong-ui-public/analytics-chart

- type: `string`
- required: `false`
- default: `'400px'`
- default: `'500px'`
- set the chart height using css height values (px, %, etc...)

#### `width`
Expand Down
8 changes: 7 additions & 1 deletion packages/analytics/analytics-chart/sandbox/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,15 @@ const metricItems = [{
unit: 'bytes',
}]
// Short labels
const statusCodeLabels = [
'200', '300', '400', '500', 'This is a really long chart label to test long labels',
'200', '300', '400', '500',
]
// Long labels
// const statusCodeLabels = [
// 'getmeakong123', 'getmeakong123123', 'testservice1233', 'testtesttest123123', 'This is a really long chart label to test long labels',
// ]
const statusCodeDimensionValues = ref(new Set(statusCodeLabels))
const serviceDimensionValues = ref(new Set([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<div
v-else
class="analytics-chart-parent"
:style="{ height: heightRef, width }"
>
<TimeSeriesChart
v-if="isTimeSeriesChart"
Expand All @@ -56,7 +57,6 @@
:dimension-axes-title="timestampAxisTitle"
:fill="chartOptions.fill"
:granularity="timeSeriesGranularity"
:height="height"
:legend-values="legendValues"
:metric-axes-title="metricAxesTitle"
:metric-unit="computedMetricUnit"
Expand All @@ -66,7 +66,6 @@
:time-range-sec="timeRangeSec"
:tooltip-title="tooltipTitle"
:type="(chartOptions.type as (ChartTypes.TIMESERIES_LINE | ChartTypes.TIMESERIES_BAR))"
:width="width"
/>
<StackedBarChart
v-else-if="isBarChart"
Expand All @@ -80,6 +79,7 @@
:orientation="barChartOrientation"
:synthetics-data-key="syntheticsDataKey"
:tooltip-title="tooltipTitle"
@height-update="handleHeightUpdate"
/>
<DoughnutChart
v-else-if="isDoughnutChart"
Expand All @@ -91,7 +91,6 @@
:metric-unit="computedMetricUnit"
:synthetics-data-key="syntheticsDataKey"
:tooltip-title="tooltipTitle"
:width="width"
/>
</div>
</div>
Expand All @@ -104,7 +103,7 @@ import { ChartTypes, ChartLegendPosition } from '../enums'
import StackedBarChart from './chart-types/StackedBarChart.vue'
import DoughnutChart from './chart-types/DoughnutChart.vue'
import type { PropType } from 'vue'
import { computed, provide, toRef } from 'vue'
import { computed, provide, ref, toRef } from 'vue'
import { GranularityKeys, msToGranularity } from '@kong-ui-public/analytics-utilities'
import type { AnalyticsExploreResult, AnalyticsExploreV2Result, GranularityFullObj } from '@kong-ui-public/analytics-utilities'
import { datavisPalette, hasMillisecondTimestamps } from '../utils'
Expand Down Expand Up @@ -151,7 +150,7 @@ const props = defineProps({
height: {
type: String,
required: false,
default: '400px',
default: '500px',
validator: (value: string): boolean => {
return /(\d *)(px|%)/.test(value)
},
Expand All @@ -177,6 +176,11 @@ const props = defineProps({
})
const { i18n } = composables.useI18n()
const heightRef = ref<string>(props.height)
const handleHeightUpdate = (height: number) => {
heightRef.value = `${height}px`
}
const computedChartData = computed(() => {
return isTimeSeriesChart.value
Expand Down Expand Up @@ -315,6 +319,10 @@ provide('legendPosition', toRef(props, 'legendPosition'))
margin: $kui-space-60;
padding: $kui-space-60;
.analytics-chart-parent {
overflow: hidden;
}
.chart-empty-state {
padding: $kui-space-70 $kui-space-0 $kui-space-60 $kui-space-0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
:style="{ background: fillStyle, 'border-color': strokeStyle }"
/>
<div
class="label-container"
:class="{ 'strike-through': !isDatasetVisible(datasetIndex, index) }"
>
<div
Expand All @@ -39,16 +40,17 @@

<script setup lang="ts">
import { ChartLegendPosition } from '../../enums'
import { Chart } from 'chart.js'
import { inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Chart, type LegendItem } from 'chart.js'
import { inject, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
import { KUI_SPACE_100 } from '@kong/design-tokens'
const props = defineProps({
id: {
type: String,
required: true,
},
items: {
type: Array,
type: Object as PropType<LegendItem[]>,
required: true,
},
chartInstance: {
Expand All @@ -57,9 +59,13 @@ const props = defineProps({
default: () => null,
},
})
const legendContainerRef = ref<HTMLElement>()
const legendItemsRef = ref<HTMLElement[]>([])
const shouldTruncate = ref(false)
const showValues = inject('showLegendValues', true)
const position = inject('legendPosition', ref(ChartLegendPosition.Right))
const legendItemsTracker = ref<LegendItem[]>([])
// Return the number of rows for a grid layout
// by cmparing the top position of each item.
Expand Down Expand Up @@ -95,9 +101,55 @@ const checkForWrap = () => {
}
}
watch(() => props.items, checkForWrap, { immediate: true, flush: 'post' })
// Set the grid-template-columns style based on the width of the widest item
const formatGrid = () => {
if (legendContainerRef.value && position.value === ChartLegendPosition.Bottom) {
let maxWidth = 0
legendItemsRef.value.forEach(item => {
// Each <li> has two elements: the legend and the label.
// Sum the width of each element to get the total width of the item.
const width = Array.from(item.children).reduce((total, element) => {
return total + (element as HTMLElement).offsetWidth
}, 0)
if (width > maxWidth) {
maxWidth = width
}
})
const padding = parseInt(KUI_SPACE_100, 10)
legendContainerRef.value.style.gridTemplateColumns = `repeat(auto-fit, ${maxWidth + padding}px)`
}
}
const legendItemsChanged = () => {
if (props.items.length !== legendItemsTracker.value.length) {
legendItemsTracker.value = props.items
return true
}
for (let i = 0; i < props.items.length; i++) {
if (props.items[i].text !== legendItemsTracker.value[i].text) {
legendItemsTracker.value = props.items
return true
}
}
return false
}
watch(() => props.items, () => {
checkForWrap()
if (legendItemsChanged()) {
formatGrid()
}
}, { immediate: true, flush: 'post' })
watch(() => position.value, () => {
formatGrid()
})
onMounted(() => {
legendItemsTracker.value = props.items
window.addEventListener('resize', checkForWrap)
})
Expand Down Expand Up @@ -149,16 +201,12 @@ const positionToClass = (position: `${ChartLegendPosition}`) => {
}[position]
}
const showValues = inject('showLegendValues', true)
const position = inject('legendPosition', ref(ChartLegendPosition.Right))
</script>

<style lang="scss" scoped>
.legend-container {
display: flex;
max-height: inherit;
min-width: 10%;
-ms-overflow-style: thin;
overflow-x: hidden;
overflow-y: scroll;
Expand All @@ -168,47 +216,30 @@ const position = inject('legendPosition', ref(ChartLegendPosition.Right))
&.vertical {
flex-direction: column;
max-height: 400px;
max-width: 200px;
.legend {
margin-top: $kui-space-30;
}
max-width: 15%;
word-break: break-word;
// Allow legend to expand horizontally at lower resolutions
@media (max-width: ($kui-breakpoint-phablet - 1px)) {
flex-direction: row;
height: 20%;
max-width: 100%;
padding-top: $kui-space-50;
width: 100%;
}
}
&.horizontal {
column-gap: $kui-space-10;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12ch, max-content));
justify-content: center;
max-height: $kui-space-150;
max-height: 15%;
width: 100%;
.legend {
margin-top: $kui-space-30;
}
.label {
width: 12ch;
}
.truncate-label {
max-width: 10ch;
max-width: 15ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
li {
display: flex;
justify-content: start;
}
}
&::-webkit-scrollbar-track {
Expand All @@ -227,23 +258,26 @@ const position = inject('legendPosition', ref(ChartLegendPosition.Right))
// Individual legend item
li {
align-items: start;
cursor: pointer;
display: flex;
margin-top: $kui-space-60;
.legend {
flex: 0 0 14px;
height: 3px;
margin-right: $kui-space-50;
margin-top: $kui-space-30;
}
cursor: pointer;
display: flex;
height: fit-content;
margin: $kui-space-50;
.label {
font-size: $kui-font-size-30;
line-height: $kui-font-size-50;
}
.sub-label {
font-size: $kui-font-size-20;
line-height: $kui-font-size-30;
word-break: none;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
>
<div
class="chart-container"
:style="{ height, width }"
>
<Doughnut
ref="chartInstance"
Expand Down Expand Up @@ -93,22 +92,6 @@ const props = defineProps({
required: false,
default: datavisPalette,
},
height: {
type: String,
required: false,
default: '400px',
validator: (value: string): boolean => {
return /(\d *)(px|%)/.test(value)
},
},
width: {
type: String,
required: false,
default: '100%',
validator: (value: string): boolean => {
return /(\d *)(px|%)/.test(value)
},
},
})
const { i18n } = composables.useI18n()
Expand Down
Loading

0 comments on commit dbef4ad

Please sign in to comment.