metricsChartPalette.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import {useCallback, useRef} from 'react';
  2. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  3. import theme from 'sentry/utils/theme';
  4. const CACHE_SIZE = 20; // number of palettes to cache
  5. export function createChartPalette(seriesNames: string[]): Record<string, string> {
  6. const uniqueSeriesNames = Array.from(new Set(seriesNames));
  7. // We do length - 2 to be aligned with the colors in other parts of the app (copy-pasta)
  8. // We use Math.max to avoid numbers < -1 as then `getColorPalette` returns undefined (not typesafe because of array access and casting)
  9. const chartColors =
  10. theme.charts.getColorPalette(Math.max(uniqueSeriesNames.length - 2, -1)) ??
  11. CHART_PALETTE[CHART_PALETTE.length - 1] ??
  12. [];
  13. return uniqueSeriesNames.reduce(
  14. (palette, seriesName, i) => {
  15. palette[seriesName] = chartColors[i % chartColors.length]!;
  16. return palette;
  17. },
  18. {} as Record<string, string>
  19. );
  20. }
  21. /**
  22. * **NOTE: Not yet optimized for performance, it should only be used for the metrics page with a limited amount of series**
  23. *
  24. * Runtime complexity is O(n * m) where n is the number of palettes in the cache and m is the number of seriesNames
  25. *
  26. * Creates chart palettes for the given seriesNames and caches them in a LRU cache
  27. * If a palette for the given seriesNames already exists in the cache, it will be returned
  28. * @param cache object that will contain the cached palettes
  29. * @param seriesNames names of the series for which to get the palette for
  30. * @returns an object mapping seriesNames to colors
  31. */
  32. export function getCachedChartPalette(
  33. cache: Readonly<Record<string, string>>[],
  34. seriesNames: string[]
  35. ): Readonly<Record<string, string>> {
  36. // Check if we already have a palette that includes all of the given seriesNames
  37. // We search in reverse to get the most recent palettes first
  38. let cacheIndex = -1;
  39. for (let i = cache.length - 1; i >= 0; i--) {
  40. const palette = cache[i]!;
  41. if (seriesNames.every(seriesName => seriesName in palette)) {
  42. cacheIndex = i;
  43. break;
  44. }
  45. }
  46. if (cacheIndex > -1) {
  47. const cachedPalette = cache[cacheIndex]!;
  48. if (cacheIndex !== cache.length - 1) {
  49. // Move the cached palette to the end of the cache, so it is the most recent one
  50. cache.splice(cacheIndex, 1);
  51. cache.push(cachedPalette);
  52. }
  53. return cachedPalette;
  54. }
  55. // If we do not have a palette for the given seriesNames, create one
  56. const newPalette = createChartPalette(seriesNames);
  57. // Single series palettes will always be the same, so we do not need to cache them
  58. if (seriesNames.length > 1) {
  59. cache.push(newPalette);
  60. }
  61. // Don't cache more than CACHE_SIZE palettes, so we do not create a memory leak
  62. if (cache.length > CACHE_SIZE) {
  63. const overflow = cache.length - CACHE_SIZE;
  64. cache.splice(0, overflow);
  65. }
  66. return newPalette;
  67. }
  68. /**
  69. * **NOTE: Not yet optimized for performance, it should only be used for the metrics page with a limited amount of series**
  70. */
  71. export const useGetCachedChartPalette = () => {
  72. const cacheRef = useRef<Readonly<Record<string, string>>[]>([]);
  73. return useCallback((seriesNames: string[]) => {
  74. // copy the cache to avoid mutating it
  75. return {...getCachedChartPalette(cacheRef.current, seriesNames)};
  76. }, []);
  77. };