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 };