github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/components/dashboards/charts/Chart/index.tsx (about)

     1  import ErrorPanel from "../../Error";
     2  import has from "lodash/has";
     3  import merge from "lodash/merge";
     4  import Placeholder from "../../Placeholder";
     5  import React, { useEffect, useRef, useState } from "react";
     6  import ReactEChartsCore from "echarts-for-react/lib/core";
     7  import set from "lodash/set";
     8  import useChartThemeColors from "../../../../hooks/useChartThemeColors";
     9  import useMediaMode from "../../../../hooks/useMediaMode";
    10  import useTemplateRender from "../../../../hooks/useTemplateRender";
    11  import {
    12    buildChartDataset,
    13    getColorOverride,
    14    LeafNodeData,
    15    themeColors,
    16    Width,
    17  } from "../../common";
    18  import { EChartsOption } from "echarts-for-react/src/types";
    19  import {
    20    ChartProperties,
    21    ChartProps,
    22    ChartSeries,
    23    ChartSeriesOptions,
    24    ChartTransform,
    25    ChartType,
    26  } from "../types";
    27  import { FlowType } from "../../flows/types";
    28  import { getChartComponent } from "..";
    29  import { GraphType } from "../../graphs/types";
    30  import { HierarchyType } from "../../hierarchies/types";
    31  import { registerComponent } from "../../index";
    32  import { useDashboard } from "../../../../hooks/useDashboard";
    33  import { useNavigate } from "react-router-dom";
    34  
    35  const getThemeColorsWithPointOverrides = (
    36    type: ChartType = "column",
    37    series: any[],
    38    seriesOverrides: ChartSeries | undefined,
    39    dataset: any[][],
    40    themeColorValues
    41  ) => {
    42    switch (type) {
    43      case "donut":
    44      case "pie": {
    45        const newThemeColors: string[] = [];
    46        for (let rowIndex = 1; rowIndex < dataset.length; rowIndex++) {
    47          if (rowIndex - 1 < themeColors.length) {
    48            newThemeColors.push(themeColors[rowIndex - 1]);
    49          } else {
    50            newThemeColors.push(themeColors[(rowIndex - 1) % themeColors.length]);
    51          }
    52        }
    53        series.forEach((seriesInfo) => {
    54          const seriesName = seriesInfo.name;
    55          const overrides = seriesOverrides
    56            ? seriesOverrides[seriesName] || {}
    57            : ({} as ChartSeriesOptions);
    58          const pointOverrides = overrides.points || {};
    59          dataset.slice(1).forEach((dataRow, dataRowIndex) => {
    60            const pointOverride = pointOverrides[dataRow[0]];
    61            if (pointOverride && pointOverride.color) {
    62              newThemeColors[dataRowIndex] = getColorOverride(
    63                pointOverride.color,
    64                themeColorValues
    65              );
    66            }
    67          });
    68        });
    69        return newThemeColors;
    70      }
    71      default:
    72        const newThemeColors: string[] = [];
    73        for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex++) {
    74          if (seriesIndex < themeColors.length - 1) {
    75            newThemeColors.push(themeColors[seriesIndex]);
    76          } else {
    77            newThemeColors.push(themeColors[seriesIndex % themeColors.length]);
    78          }
    79        }
    80        return newThemeColors;
    81    }
    82  };
    83  
    84  const getCommonBaseOptions = () => ({
    85    animation: false,
    86    grid: {
    87      bottom: 40,
    88      containLabel: true,
    89    },
    90    legend: {
    91      orient: "horizontal",
    92      left: "center",
    93      top: "10",
    94      textStyle: {
    95        fontSize: 11,
    96      },
    97    },
    98    textStyle: {
    99      fontFamily:
   100        'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
   101    },
   102    tooltip: {
   103      appendToBody: true,
   104      textStyle: {
   105        fontSize: 11,
   106      },
   107      trigger: "item",
   108    },
   109  });
   110  
   111  const getXAxisLabelRotation = (number_of_rows: number) => {
   112    if (number_of_rows < 5) {
   113      return 0;
   114    }
   115    if (number_of_rows < 10) {
   116      return 30;
   117    }
   118    if (number_of_rows < 15) {
   119      return 45;
   120    }
   121    if (number_of_rows < 20) {
   122      return 60;
   123    }
   124    return 90;
   125  };
   126  
   127  const getXAxisLabelWidth = (number_of_rows: number) => {
   128    if (number_of_rows < 5) {
   129      return null;
   130    }
   131    if (number_of_rows < 10) {
   132      return 85;
   133    }
   134    if (number_of_rows < 15) {
   135      return 75;
   136    }
   137    if (number_of_rows < 20) {
   138      return 60;
   139    }
   140    return 50;
   141  };
   142  
   143  const getCommonBaseOptionsForChartType = (
   144    type: ChartType | undefined,
   145    width: Width | undefined,
   146    dataset: any[][],
   147    shouldBeTimeSeries: boolean,
   148    series: any[],
   149    seriesOverrides: ChartSeries | undefined,
   150    themeColors
   151  ) => {
   152    switch (type) {
   153      case "bar":
   154        return {
   155          color: getThemeColorsWithPointOverrides(
   156            type,
   157            series,
   158            seriesOverrides,
   159            dataset,
   160            themeColors
   161          ),
   162          legend: {
   163            show: series ? series.length > 1 : false,
   164            textStyle: {
   165              color: themeColors.foreground,
   166            },
   167          },
   168          // Declare an x-axis (category axis).
   169          // The category map the first row in the dataset by default.
   170          xAxis: {
   171            axisLabel: { color: themeColors.foreground },
   172            axisLine: {
   173              show: true,
   174              lineStyle: { color: themeColors.foregroundLightest },
   175            },
   176            axisTick: { show: true },
   177            nameGap: 30,
   178            nameLocation: "center",
   179            nameTextStyle: { color: themeColors.foreground },
   180            splitLine: { show: false },
   181          },
   182          // Declare a y-axis (value axis).
   183          yAxis: {
   184            type: "category",
   185            axisLabel: {
   186              color: themeColors.foreground,
   187              width: 50,
   188              overflow: "truncate",
   189            },
   190            axisLine: { lineStyle: { color: themeColors.foregroundLightest } },
   191            axisTick: { show: false },
   192            nameGap: width ? width + 42 : 50,
   193            nameLocation: "center",
   194            nameTextStyle: { color: themeColors.foreground },
   195          },
   196        };
   197      case "area":
   198      case "line":
   199        return {
   200          color: getThemeColorsWithPointOverrides(
   201            type,
   202            series,
   203            seriesOverrides,
   204            dataset,
   205            themeColors
   206          ),
   207          legend: {
   208            show: series ? series.length > 1 : false,
   209            textStyle: {
   210              color: themeColors.foreground,
   211            },
   212          },
   213          // Declare an x-axis (category or time axis, depending on the type of the first column).
   214          // The category/time map the first row in the dataset by default.
   215          xAxis: {
   216            type: shouldBeTimeSeries ? "time" : "category",
   217            boundaryGap: type !== "area",
   218            axisLabel: {
   219              color: themeColors.foreground,
   220              rotate: getXAxisLabelRotation(dataset.length - 1),
   221              width: getXAxisLabelWidth(dataset.length),
   222              overflow: "truncate",
   223            },
   224            axisLine: { lineStyle: { color: themeColors.foregroundLightest } },
   225            axisTick: { show: false },
   226            nameGap: 30,
   227            nameLocation: "center",
   228            nameTextStyle: { color: themeColors.foreground },
   229          },
   230          // Declare a y-axis (value axis).
   231          yAxis: {
   232            axisLabel: { color: themeColors.foreground },
   233            axisLine: {
   234              show: true,
   235              lineStyle: { color: themeColors.foregroundLightest },
   236            },
   237            axisTick: { show: true },
   238            splitLine: { show: false },
   239            nameGap: width ? width + 42 : 50,
   240            nameLocation: "center",
   241            nameTextStyle: { color: themeColors.foreground },
   242          },
   243          tooltip: {
   244            trigger: "axis",
   245          },
   246        };
   247      case "column":
   248        return {
   249          color: getThemeColorsWithPointOverrides(
   250            type,
   251            series,
   252            seriesOverrides,
   253            dataset,
   254            themeColors
   255          ),
   256          legend: {
   257            show: series ? series.length > 1 : false,
   258            textStyle: {
   259              color: themeColors.foreground,
   260            },
   261          },
   262          // Declare an x-axis (category or time axis, depending on the value of the first column).
   263          // The category/time map the first row in the dataset by default.
   264          xAxis: {
   265            type: shouldBeTimeSeries ? "time" : "category",
   266            axisLabel: {
   267              color: themeColors.foreground,
   268              rotate: getXAxisLabelRotation(dataset.length - 1),
   269              width: getXAxisLabelWidth(dataset.length),
   270              overflow: "truncate",
   271            },
   272            axisLine: { lineStyle: { color: themeColors.foregroundLightest } },
   273            axisTick: { show: false },
   274            nameGap: 30,
   275            nameLocation: "center",
   276            nameTextStyle: { color: themeColors.foreground },
   277          },
   278          // Declare a y-axis (value axis).
   279          yAxis: {
   280            axisLabel: { color: themeColors.foreground },
   281            axisLine: {
   282              show: true,
   283              lineStyle: { color: themeColors.foregroundLightest },
   284            },
   285            axisTick: { show: true },
   286            splitLine: { show: false },
   287            nameGap: width ? width + 42 : 50,
   288            nameLocation: "center",
   289            nameTextStyle: { color: themeColors.foreground },
   290          },
   291          ...(shouldBeTimeSeries ? {tooltip: {trigger: "axis"}} : {})
   292        };
   293      case "pie":
   294        return {
   295          color: getThemeColorsWithPointOverrides(
   296            type,
   297            series,
   298            seriesOverrides,
   299            dataset,
   300            themeColors
   301          ),
   302          legend: {
   303            show: false,
   304            textStyle: {
   305              color: themeColors.foreground,
   306            },
   307          },
   308        };
   309      case "donut":
   310        return {
   311          color: getThemeColorsWithPointOverrides(
   312            type,
   313            series,
   314            seriesOverrides,
   315            dataset,
   316            themeColors
   317          ),
   318          legend: {
   319            show: false,
   320            textStyle: {
   321              color: themeColors.foreground,
   322            },
   323          },
   324        };
   325      default:
   326        return {};
   327    }
   328  };
   329  
   330  const getOptionOverridesForChartType = (
   331    type: ChartType = "column",
   332    properties: ChartProperties | undefined,
   333    shouldBeTimeSeries: boolean,
   334  ) => {
   335    if (!properties) {
   336      return {};
   337    }
   338  
   339    let overrides = {};
   340  
   341    // orient: "horizontal",
   342    //     left: "center",
   343    //     top: "top",
   344  
   345    if (properties.legend) {
   346      // Legend display
   347      const legendDisplay = properties.legend.display;
   348      if (legendDisplay === "all") {
   349        overrides = set(overrides, "legend.show", true);
   350      } else if (legendDisplay === "none") {
   351        overrides = set(overrides, "legend.show", false);
   352      }
   353  
   354      // Legend display position
   355      const legendPosition = properties.legend.position;
   356      if (legendPosition === "top") {
   357        overrides = set(overrides, "legend.orient", "horizontal");
   358        overrides = set(overrides, "legend.left", "center");
   359        overrides = set(overrides, "legend.top", 10);
   360        overrides = set(overrides, "legend.bottom", "auto");
   361      } else if (legendPosition === "right") {
   362        overrides = set(overrides, "legend.orient", "vertical");
   363        overrides = set(overrides, "legend.left", "right");
   364        overrides = set(overrides, "legend.top", "middle");
   365        overrides = set(overrides, "legend.bottom", "auto");
   366        overrides = set(overrides, "grid.right", "20%");
   367      } else if (legendPosition === "bottom") {
   368        overrides = set(overrides, "legend.orient", "horizontal");
   369        overrides = set(overrides, "legend.left", "center");
   370        overrides = set(overrides, "legend.top", "auto");
   371        overrides = set(overrides, "legend.bottom", 10);
   372        overrides = set(overrides, "grid.top", 30);
   373      } else if (legendPosition === "left") {
   374        overrides = set(overrides, "legend.orient", "vertical");
   375        overrides = set(overrides, "legend.left", "left");
   376        overrides = set(overrides, "legend.top", "middle");
   377        overrides = set(overrides, "legend.bottom", "auto");
   378        overrides = set(overrides, "grid.left", "20%");
   379      }
   380    }
   381  
   382    // Axes settings
   383    if (properties.axes) {
   384      // X axis settings
   385      if (properties.axes.x) {
   386        // X axis display setting
   387        const xAxisDisplay = properties.axes.x.display;
   388        if (xAxisDisplay === "all") {
   389          overrides = set(overrides, "xAxis.show", true);
   390        } else if (xAxisDisplay === "none") {
   391          overrides = set(overrides, "xAxis.show", false);
   392        }
   393  
   394        // X axis min setting
   395        if (type === "bar" && has(properties, "axes.x.min")) {
   396          overrides = set(overrides, "xAxis.min", properties.axes.x.min);
   397        }
   398        // Y axis max setting
   399        if (type === "bar" && has(properties, "axes.x.max")) {
   400          overrides = set(overrides, "xAxis.max", properties.axes.x.max);
   401        }
   402  
   403        // X axis labels settings
   404        if (properties.axes.x.labels) {
   405          // X axis labels display setting
   406          const xAxisTicksDisplay = properties.axes.x.labels.display;
   407          if (xAxisTicksDisplay === "all") {
   408            overrides = set(overrides, "xAxis.axisLabel.show", true);
   409          } else if (xAxisTicksDisplay === "none") {
   410            overrides = set(overrides, "xAxis.axisLabel.show", false);
   411          }
   412        }
   413  
   414        // X axis title settings
   415        if (properties.axes.x.title) {
   416          // X axis title display setting
   417          const xAxisTitleDisplay = properties.axes.x.title.display;
   418          if (xAxisTitleDisplay === "none") {
   419            overrides = set(overrides, "xAxis.name", null);
   420          }
   421  
   422          // X Axis title align setting
   423          const xAxisTitleAlign = properties.axes.x.title.align;
   424          if (xAxisTitleAlign === "start") {
   425            overrides = set(overrides, "xAxis.nameLocation", "start");
   426          } else if (xAxisTitleAlign === "center") {
   427            overrides = set(overrides, "xAxis.nameLocation", "center");
   428          } else if (xAxisTitleAlign === "end") {
   429            overrides = set(overrides, "xAxis.nameLocation", "end");
   430          }
   431  
   432          // X Axis title value setting
   433          const xAxisTitleValue = properties.axes.x.title.value;
   434          if (xAxisTitleValue) {
   435            overrides = set(overrides, "xAxis.name", xAxisTitleValue);
   436          }
   437        }
   438  
   439        // X Axis range setting (for timeseries plots)
   440        // Valid chart types: column, area, line (bar, donut and pie make no sense)
   441        if (["column", "area", "line"].includes(type) && shouldBeTimeSeries) {
   442          // X axis min setting (for timeseries)
   443          if (has(properties, "axes.x.min")) {
   444            // ECharts wants millis since epoch, not seconds
   445            overrides = set(overrides, "xAxis.min", properties.axes.x.min * 1000); 
   446          }
   447          // Y axis max setting (for timeseries)
   448          if (has(properties, "axes.x.max")) {
   449            // ECharts wants millis since epoch, not seconds
   450            overrides = set(overrides, "xAxis.max", properties.axes.x.max * 1000);
   451          }
   452        }
   453      }
   454  
   455      // Y axis settings
   456      if (properties.axes.y) {
   457        // Y axis display setting
   458        const yAxisDisplay = properties.axes.y.display;
   459        if (yAxisDisplay === "all") {
   460          overrides = set(overrides, "yAxis.show", true);
   461        } else if (yAxisDisplay === "none") {
   462          overrides = set(overrides, "yAxis.show", false);
   463        }
   464  
   465        // Y axis min setting
   466        if (type !== "bar" && has(properties, "axes.y.min")) {
   467          overrides = set(overrides, "yAxis.min", properties.axes.y.min);
   468        }
   469        // Y axis max setting
   470        if (type !== "bar" && has(properties, "axes.y.max")) {
   471          overrides = set(overrides, "yAxis.max", properties.axes.y.max);
   472        }
   473  
   474        // Y axis labels settings
   475        if (properties.axes.y.labels) {
   476          // Y axis labels display setting
   477          const yAxisTicksDisplay = properties.axes.y.labels.display;
   478          if (yAxisTicksDisplay === "all") {
   479            overrides = set(overrides, "yAxis.axisLabel.show", true);
   480          } else if (yAxisTicksDisplay === "none") {
   481            overrides = set(overrides, "yAxis.axisLabel.show", false);
   482          }
   483        }
   484  
   485        // Y axis title settings
   486        if (properties.axes.y.title) {
   487          // Y axis title display setting
   488          const yAxisTitleDisplay = properties.axes.y.title.display;
   489          if (yAxisTitleDisplay === "none") {
   490            overrides = set(overrides, "yAxis.name", null);
   491          }
   492  
   493          // Y Axis title align setting
   494          const yAxisTitleAlign = properties.axes.y.title.align;
   495          if (yAxisTitleAlign === "start") {
   496            overrides = set(overrides, "yAxis.nameLocation", "start");
   497          } else if (yAxisTitleAlign === "center") {
   498            overrides = set(overrides, "yAxis.nameLocation", "center");
   499          } else if (yAxisTitleAlign === "end") {
   500            overrides = set(overrides, "yAxis.nameLocation", "end");
   501          }
   502  
   503          // Y Axis title value setting
   504          const yAxisTitleValue = properties.axes.y.title.value;
   505          if (yAxisTitleValue) {
   506            overrides = set(overrides, "yAxis.name", yAxisTitleValue);
   507          }
   508        }
   509      }
   510    }
   511  
   512    return overrides;
   513  };
   514  
   515  const getSeriesForChartType = (
   516    type: ChartType = "column",
   517    data: LeafNodeData | undefined,
   518    properties: ChartProperties | undefined,
   519    rowSeriesLabels: string[],
   520    transform: ChartTransform,
   521    shouldBeTimeSeries: boolean,
   522    themeColors
   523  ) => {
   524    if (!data) {
   525      return [];
   526    }
   527    const series: any[] = [];
   528    const seriesNames =
   529      transform === "crosstab"
   530        ? rowSeriesLabels
   531        : data.columns.slice(1).map((col) => col.name);
   532    const seriesLength = seriesNames.length;
   533    for (let seriesIndex = 0; seriesIndex < seriesLength; seriesIndex++) {
   534      let seriesName = seriesNames[seriesIndex];
   535      let seriesColor = "auto";
   536      let seriesOverrides;
   537      if (properties) {
   538        if (properties.series && properties.series[seriesName]) {
   539          seriesOverrides = properties.series[seriesName];
   540        }
   541        if (seriesOverrides && seriesOverrides.title) {
   542          seriesName = seriesOverrides.title;
   543        }
   544        if (seriesOverrides && seriesOverrides.color) {
   545          seriesColor = getColorOverride(seriesOverrides.color, themeColors);
   546        }
   547      }
   548  
   549      switch (type) {
   550        case "bar":
   551        case "column":
   552          series.push({
   553            name: seriesName,
   554            type: "bar",
   555            ...(properties && properties.grouping === "compare"
   556              ? {}
   557              : { stack: "total" }),
   558            itemStyle: { color: seriesColor },
   559            // Per https://stackoverflow.com/a/56116442, when using time series you have to manually encode each series
   560            // We assume that the first dimension/column is the timestamp
   561            ...(shouldBeTimeSeries
   562              ? {encode: {x: 0, y: seriesName}}
   563              : {}),
   564            // label: {
   565            //   show: true,
   566            //   position: 'outside'
   567            // },
   568          });
   569          break;
   570        case "donut":
   571          series.push({
   572            name: seriesName,
   573            type: "pie",
   574            center: ["50%", "45%"],
   575            radius: ["30%", "50%"],
   576            label: { color: themeColors.foreground },
   577          });
   578          break;
   579        case "area":
   580          series.push({
   581            name: seriesName,
   582            type: "line",
   583            ...(properties && properties.grouping === "compare"
   584              ? {}
   585              : { stack: "total" }),
   586            // Per https://stackoverflow.com/a/56116442, when using time series you have to manually encode each series
   587            // We assume that the first dimension/column is the timestamp
   588            ...(shouldBeTimeSeries
   589              ? {encode: {x: 0, y: seriesName}}
   590              : {}),
   591            areaStyle: {},
   592            emphasis: {
   593              focus: "series",
   594            },
   595            itemStyle: { color: seriesColor },
   596          });
   597          break;
   598        case "line":
   599          series.push({
   600            name: seriesName,
   601            type: "line",
   602            itemStyle: { color: seriesColor },
   603            // Per https://stackoverflow.com/a/56116442, when using time series you have to manually encode each series
   604            // We assume that the first dimension/column is the timestamp
   605            ...(shouldBeTimeSeries
   606              ? {encode: {x: 0, y: seriesName}}
   607              : {}),
   608          });
   609          break;
   610        case "pie":
   611          series.push({
   612            name: seriesName,
   613            type: "pie",
   614            center: ["50%", "40%"],
   615            radius: "50%",
   616            label: { color: themeColors.foreground },
   617            emphasis: {
   618              itemStyle: {
   619                shadowBlur: 5,
   620                shadowOffsetX: 0,
   621                shadowColor: "rgba(0, 0, 0, 0.5)",
   622              },
   623            },
   624          });
   625      }
   626    }
   627    return series;
   628  };
   629  
   630  const buildChartOptions = (props: ChartProps, themeColors: any) => {
   631    const { dataset, rowSeriesLabels, transform } = buildChartDataset(
   632      props.data,
   633      props.properties
   634    );
   635    const treatAsTimeSeries = ["timestamp", "timestamptz", "date"].includes(props.data?.columns[0].data_type.toLowerCase() || "")
   636    const series = getSeriesForChartType(
   637      props.display_type || "column",
   638      props.data,
   639      props.properties,
   640      rowSeriesLabels,
   641      transform,
   642      treatAsTimeSeries,
   643      themeColors
   644    );
   645    return merge(
   646      getCommonBaseOptions(),
   647      getCommonBaseOptionsForChartType(
   648        props.display_type || "column",
   649        props.width,
   650        dataset,
   651        treatAsTimeSeries,
   652        series,
   653        props.properties?.series,
   654        themeColors
   655      ),
   656      getOptionOverridesForChartType(
   657        props.display_type || "column",
   658        props.properties,
   659        treatAsTimeSeries,
   660      ),
   661      { series },
   662      {
   663        dataset: {
   664          source: dataset,
   665        },
   666      }
   667    );
   668  };
   669  
   670  type ChartComponentProps = {
   671    options: EChartsOption;
   672    type: ChartType | FlowType | GraphType | HierarchyType;
   673  };
   674  
   675  const handleClick = async (params: any, navigate, renderTemplates) => {
   676    const componentType = params.componentType;
   677    if (componentType !== "series") {
   678      return;
   679    }
   680    const dataType = params.dataType;
   681  
   682    switch (dataType) {
   683      case "node":
   684        if (!params.data.href) {
   685          return;
   686        }
   687        const renderedResults = await renderTemplates(
   688          { graph_node: params.data.href as string },
   689          [params.data]
   690        );
   691        let rowRenderResult = renderedResults[0];
   692        navigate(rowRenderResult.graph_node.result);
   693    }
   694  };
   695  
   696  const Chart = ({ options, type }: ChartComponentProps) => {
   697    const [echarts, setEcharts] = useState<any | null>(null);
   698    const navigate = useNavigate();
   699    const chartRef = useRef<ReactEChartsCore>(null);
   700    const [imageUrl, setImageUrl] = useState<string | null>(null);
   701    const mediaMode = useMediaMode();
   702    const { ready: templateRenderReady, renderTemplates } = useTemplateRender();
   703  
   704    // Dynamically import echarts from its own bundle
   705    useEffect(() => {
   706      import("./echarts").then((m) => setEcharts(m.echarts));
   707    }, []);
   708  
   709    useEffect(() => {
   710      if (!chartRef.current || !options) {
   711        return;
   712      }
   713  
   714      const echartInstance = chartRef.current.getEchartsInstance();
   715      const dataURL = echartInstance.getDataURL({});
   716      if (dataURL === imageUrl) {
   717        return;
   718      }
   719      setImageUrl(dataURL);
   720    }, [chartRef, imageUrl, options]);
   721  
   722    if (!options) {
   723      return null;
   724    }
   725  
   726    const eventsDict = {
   727      click: (params) => handleClick(params, navigate, renderTemplates),
   728    };
   729  
   730    const PlaceholderComponent = Placeholder.component;
   731  
   732    return (
   733      <PlaceholderComponent ready={!!echarts && templateRenderReady}>
   734        <>
   735          {mediaMode !== "print" && (
   736            <div className="relative">
   737              <ReactEChartsCore
   738                ref={chartRef}
   739                echarts={echarts}
   740                className="chart-canvas"
   741                onEvents={eventsDict}
   742                option={options}
   743                notMerge={true}
   744                lazyUpdate={true}
   745                style={
   746                  type === "pie" || type === "donut" ? { height: "250px" } : {}
   747                }
   748              />
   749            </div>
   750          )}
   751          {mediaMode === "print" && imageUrl && (
   752            <div>
   753              <img alt="Chart" className="max-w-full max-h-full" src={imageUrl} />
   754            </div>
   755          )}
   756        </>
   757      </PlaceholderComponent>
   758    );
   759  };
   760  
   761  const ChartWrapper = (props: ChartProps) => {
   762    const {
   763      themeContext: { wrapperRef },
   764    } = useDashboard();
   765    const themeColors = useChartThemeColors();
   766  
   767    if (!wrapperRef) {
   768      return null;
   769    }
   770  
   771    if (!props.data) {
   772      return null;
   773    }
   774  
   775    return (
   776      <Chart
   777        options={buildChartOptions(props, themeColors)}
   778        type={props.display_type || "column"}
   779      />
   780    );
   781  };
   782  
   783  const renderChart = (definition: ChartProps) => {
   784    // We default to column charts if not specified
   785    const { display_type = "column" } = definition;
   786  
   787    const chart = getChartComponent(display_type);
   788  
   789    if (!chart) {
   790      return <ErrorPanel error={`Unknown chart type ${display_type}`} />;
   791    }
   792  
   793    const Component = chart.component;
   794    return <Component {...definition} />;
   795  };
   796  
   797  const RenderChart = (props: ChartProps) => {
   798    return renderChart(props);
   799  };
   800  
   801  registerComponent("chart", RenderChart);
   802  
   803  export default ChartWrapper;
   804  
   805  export { Chart };