metricsChartPalette.tsx 3.1 KB

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