Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"@uiw/react-codemirror": "4.23.7",
"@xyflow/react": "12.4.2",
"ansi_up": "^5.2.1",
"chart.js": "^4.5.0",
"codemirror-json-schema": "0.8.0",
"dayjs": "^1.11.13",
"fast-json-patch": "^3.1.1",
Expand Down
76 changes: 76 additions & 0 deletions src/Shared/Components/Charts/Chart.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react'
import {
ArcElement,
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
DoughnutController,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Title,
Tooltip,
} from 'chart.js'

import { LEGENDS_LABEL_CONFIG } from './constants'
import { ChartProps } from './types'
import { getChartJSType, getDefaultOptions, transformDataForChart } from './utils'

// Register Chart.js components
ChartJS.register(
CategoryScale,
LinearScale,
BarController,
BarElement,
LineController,
LineElement,
PointElement,
DoughnutController,
ArcElement,
Title,
Tooltip,
Legend,
Filler,
)

ChartJS.overrides.doughnut.plugins.legend.labels = {
...ChartJS.overrides.doughnut.plugins.legend.labels,
...LEGENDS_LABEL_CONFIG,
}

const Chart = ({ id, type, labels, datasets, className, style }: ChartProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const chartRef = useRef<ChartJS | null>(null)

useEffect(() => {
const ctx = canvasRef.current.getContext('2d')

// Get Chart.js type and transform data
const chartJSType = getChartJSType(type)
const transformedData = transformDataForChart(labels, datasets, type)
const defaultOptions = getDefaultOptions(type)

// Create new chart
chartRef.current = new ChartJS(ctx, {
type: chartJSType,
data: transformedData,
options: defaultOptions,
})

return () => {
chartRef.current?.destroy()
}
}, [type, datasets, labels])

return (
<div className="flex" style={style}>
<canvas id={id} ref={canvasRef} className={className} />
</div>
)
}

export default Chart
11 changes: 11 additions & 0 deletions src/Shared/Components/Charts/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const LEGENDS_LABEL_CONFIG = {
usePointStyle: true,
pointStyle: 'rectRounded',
pointStyleWidth: 0,
font: {
family: "'IBM Plex Sans', 'Open Sans', 'Roboto'",
size: 13,
lineHeight: '150%',
weight: 400,
},
} as const
2 changes: 2 additions & 0 deletions src/Shared/Components/Charts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Chart } from './Chart.component'
export type { ChartProps, SimpleDataset } from './types'
15 changes: 15 additions & 0 deletions src/Shared/Components/Charts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type ChartType = 'area' | 'pie' | 'stackedBar' | 'stackedBarHorizontal'

export interface SimpleDataset {
label: string
data: number[]
}

export interface ChartProps {
id: string
type: ChartType
labels: string[]
datasets: SimpleDataset[]
className?: string
style?: React.CSSProperties
}
208 changes: 208 additions & 0 deletions src/Shared/Components/Charts/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { ChartData, ChartDataset, ChartOptions, ChartType as ChartJSChartType } from 'chart.js'

import { LEGENDS_LABEL_CONFIG } from './constants'
import { ChartType, SimpleDataset } from './types'

const getCSSVariableValue = (variableName: string) => {
const value = getComputedStyle(document.querySelector('#devtron-base-main-identifier')).getPropertyValue(
variableName,
)

if (!value) {
// eslint-disable-next-line no-console
console.error(`CSS variable "${variableName}" not found`)
}

return value ?? 'transparent'
}

// Map our chart types to Chart.js types
export const getChartJSType = (type: ChartType): ChartJSChartType => {
switch (type) {
case 'area':
return 'line'
case 'pie':
return 'doughnut'
case 'stackedBar':
case 'stackedBarHorizontal':
return 'bar'
default:
return type as ChartJSChartType
}
}

// Get default options based on chart type
export const getDefaultOptions = (type: ChartType): ChartOptions => {
const baseOptions: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
devicePixelRatio: 3,
plugins: {
legend: {
position: 'bottom' as const,
labels: LEGENDS_LABEL_CONFIG,
},
title: {
display: false,
},
},
elements: {
line: {
fill: true,
tension: 0.4,
},
bar: {
borderSkipped: 'start' as const,
borderWidth: 2,
borderColor: 'transparent',
borderRadius: 4,
},
arc: {
spacing: 2,
},
},
}

const gridConfig = {
color: getCSSVariableValue('--N50'),
}

switch (type) {
case 'area':
return {
...baseOptions,
plugins: {
...baseOptions.plugins,
tooltip: {
mode: 'index',
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
scales: {
y: {
stacked: true,
beginAtZero: true,
grid: gridConfig,
},
x: {
grid: gridConfig,
},
},
} as ChartOptions<'line'>
case 'stackedBar':
return {
...baseOptions,
scales: {
x: {
stacked: true,
grid: gridConfig,
},
y: {
stacked: true,
beginAtZero: true,
grid: gridConfig,
},
},
}
case 'stackedBarHorizontal':
return {
...baseOptions,
indexAxis: 'y' as const,
scales: {
x: {
stacked: true,
beginAtZero: true,
grid: gridConfig,
},
y: {
stacked: true,
grid: gridConfig,
},
},
}
case 'pie':
return {
...baseOptions,
plugins: {
...baseOptions.plugins,
legend: {
position: 'right',
align: 'center',
},
},
}
default:
return baseOptions
}
}

// Generates a palette of pastel HSL colors
const generateColors = (count: number): string[] => {
const colors: string[] = []
for (let i = 0; i < count; i++) {
const hue = (i * 360) / count
const saturation = 50 // Pastel: 40-60%
const lightness = 75 // Pastel: 80-90%
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`)
}
return colors
}

// Generates a slightly darker shade for a given HSL color string
const generateCorrespondingBorderColor = (hsl: string): string => {
// Parse hsl string: hsl(hue, saturation%, lightness%)
const match = hsl.match(/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/)
if (!match) throw new Error('Invalid HSL color format')
const hue = Number(match[1])
const saturation = Number(match[2])
let lightness = Number(match[3])
lightness = Math.max(0, lightness - 15) // Clamp to 0
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
}

// Transform simple data to Chart.js format with consistent styling
export const transformDataForChart = (labels: string[], datasets: SimpleDataset[], type: ChartType): ChartData => {
const colors = generateColors(type === 'pie' ? datasets[0].data.length : datasets.length)

const transformedDatasets = datasets.map((dataset, index) => {
const colorIndex = index % colors.length
const baseDataset = {
label: dataset.label,
data: dataset.data,
backgroundColor: colors[colorIndex],
}

switch (type) {
case 'area':
return {
...baseDataset,
fill: true,
pointRadius: 0,
pointHoverRadius: 10,
pointHitRadius: 20,
pointStyle: 'rectRounded',
pointBorderWidth: 0,
borderWidth: 2,
borderColor: generateCorrespondingBorderColor(colors[colorIndex]),
} as ChartDataset<'line'>
case 'pie':
return {
...baseDataset,
backgroundColor: colors.slice(0, dataset.data.length),
}
case 'stackedBar':
case 'stackedBarHorizontal':
default:
return baseDataset
}
})

return {
labels,
datasets: transformedDatasets,
}
}
1 change: 1 addition & 0 deletions src/Shared/Components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export * from './BulkSelection'
export * from './Button'
export * from './ButtonWithLoader'
export * from './ButtonWithSelector'
export * from './Charts'
export * from './Chip'
export * from './CICDHistory'
export * from './CMCS'
Expand Down