@@ -26,6 +26,7 @@ import {
26
26
VerticalBarChartDataPoint ,
27
27
SankeyChartData ,
28
28
LineChartLineOptions ,
29
+ GanttChartDataPoint ,
29
30
} from '../../types/DataPoint' ;
30
31
import { SankeyChartProps } from '../SankeyChart/index' ;
31
32
import { VerticalStackedBarChartProps } from '../VerticalStackedBarChart/index' ;
@@ -86,6 +87,7 @@ import { rgb } from 'd3-color';
86
87
import { Legend , LegendsProps } from '../Legends/index' ;
87
88
import { ScatterChartProps } from '../ScatterChart/ScatterChart.types' ;
88
89
import { CartesianChartProps } from '../CommonComponents/index' ;
90
+ import { FunnelChartDataPoint , FunnelChartProps } from '../FunnelChart/FunnelChart.types' ;
89
91
90
92
export const NON_PLOT_KEY_PREFIX = 'nonplot_' ;
91
93
export const SINGLE_REPEAT = 'repeat(1, 1fr)' ;
@@ -1210,6 +1212,83 @@ export const transformPlotlyJsonToHorizontalBarWithAxisProps = (
1210
1212
} ;
1211
1213
} ;
1212
1214
1215
+ export const transformPlotlyJsonToGanttChartProps = (
1216
+ input : PlotlySchema ,
1217
+ isMultiPlot : boolean ,
1218
+ colorMap : React . MutableRefObject < Map < string , string > > ,
1219
+ colorwayType : ColorwayType ,
1220
+ isDarkTheme ?: boolean ,
1221
+ ) : GanttChartProps => {
1222
+ const { legends, hideLegend } = getLegendProps ( input . data , input . layout , isMultiPlot ) ;
1223
+ let colorScale : ( ( value : number ) => string ) | undefined = undefined ;
1224
+ const chartData : GanttChartDataPoint [ ] = input . data
1225
+ . map ( ( series : Partial < PlotData > , index : number ) => {
1226
+ colorScale = createColorScale ( input . layout , series , colorScale ) ;
1227
+
1228
+ // extract colors for each series only once
1229
+ const extractedColors = extractColor (
1230
+ input . layout ?. template ?. layout ?. colorway ,
1231
+ colorwayType ,
1232
+ series . marker ?. color ,
1233
+ colorMap ,
1234
+ isDarkTheme ,
1235
+ ) as string [ ] | string | undefined ;
1236
+ const legend = legends [ index ] ;
1237
+ const isXDate = input . layout ?. xaxis ?. type === 'date' || isDateArray ( series . x ) ;
1238
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1239
+ const convertXValueToNumber = ( value : any ) => {
1240
+ return isInvalidValue ( value ) ? 0 : isXDate ? + parseLocalDate ( value ) : + value ;
1241
+ } ;
1242
+
1243
+ return ( series . y as Datum [ ] )
1244
+ . map ( ( yVal , i : number ) => {
1245
+ if ( isInvalidValue ( yVal ) ) {
1246
+ return null ;
1247
+ }
1248
+ // resolve color for each legend's bars from the colorscale or extracted colors
1249
+ const color = colorScale
1250
+ ? colorScale (
1251
+ isArrayOrTypedArray ( series . marker ?. color )
1252
+ ? ( ( series . marker ?. color as Color [ ] ) ?. [ i % ( series . marker ?. color as Color [ ] ) . length ] as number )
1253
+ : 0 ,
1254
+ )
1255
+ : resolveColor ( extractedColors , i , legend , colorMap , isDarkTheme ) ;
1256
+ const opacity = getOpacity ( series , i ) ;
1257
+ const base = convertXValueToNumber ( series . base ?. [ i ] ) ;
1258
+ const xVal = convertXValueToNumber ( series . x ?. [ i ] ) ;
1259
+
1260
+ return {
1261
+ x : {
1262
+ start : isXDate ? new Date ( base ) : base ,
1263
+ end : isXDate ? new Date ( base + xVal ) : base + xVal ,
1264
+ } ,
1265
+ y : yVal ,
1266
+ legend,
1267
+ color : rgb ( color ) . copy ( { opacity } ) . formatHex8 ( ) ?? color ,
1268
+ } as GanttChartDataPoint ;
1269
+ } )
1270
+ . filter ( point => point !== null ) as GanttChartDataPoint [ ] ;
1271
+ } )
1272
+ . flat ( ) ;
1273
+
1274
+ return {
1275
+ data : chartData ,
1276
+ showYAxisLables : true ,
1277
+ height : input . layout ?. height ?? 350 ,
1278
+ width : input . layout ?. width ,
1279
+ hideTickOverlap : true ,
1280
+ hideLegend,
1281
+ noOfCharsToTruncate : 20 ,
1282
+ showYAxisLablesTooltip : true ,
1283
+ roundCorners : true ,
1284
+ useUTC : false ,
1285
+ ...getTitles ( input . layout ) ,
1286
+ ...getAxisCategoryOrderProps ( input . data , input . layout ) ,
1287
+ ...getBarProps ( input . data , input . layout , true ) ,
1288
+ ...getAxisTickProps ( input . data , input . layout ) ,
1289
+ } ;
1290
+ } ;
1291
+
1213
1292
export const transformPlotlyJsonToHeatmapProps = (
1214
1293
input : PlotlySchema ,
1215
1294
isMultiPlot : boolean ,
@@ -1731,6 +1810,148 @@ export const transformPlotlyJsonToChartTableProps = (
1731
1810
} ;
1732
1811
} ;
1733
1812
1813
+ function getCategoriesAndValues ( series : Partial < PlotData > ) : {
1814
+ categories : ( string | number ) [ ] ;
1815
+ values : ( string | number ) [ ] ;
1816
+ } {
1817
+ const orientation = series . orientation || 'h' ;
1818
+ const y = series . labels ?? series . y ?? series . stage ;
1819
+ const x = series . values ?? series . x ?? series . value ;
1820
+ const xIsString = isStringArray ( x as Datum [ ] | Datum [ ] [ ] | TypedArray | undefined ) ;
1821
+ const yIsString = isStringArray ( y as Datum [ ] | Datum [ ] [ ] | TypedArray | undefined ) ;
1822
+ const xIsNumber = isNumberArray ( x as Datum [ ] | Datum [ ] [ ] | TypedArray | undefined ) ;
1823
+ const yIsNumber = isNumberArray ( y as Datum [ ] | Datum [ ] [ ] | TypedArray | undefined ) ;
1824
+
1825
+ // Helper to ensure array of (string | number)
1826
+ const toArray = ( arr : unknown ) : ( string | number ) [ ] => {
1827
+ if ( Array . isArray ( arr ) ) {
1828
+ return arr as ( string | number ) [ ] ;
1829
+ }
1830
+ if ( typeof arr === 'string' || typeof arr === 'number' ) {
1831
+ return [ arr ] ;
1832
+ }
1833
+ return [ ] ;
1834
+ } ;
1835
+
1836
+ if ( orientation === 'h' ) {
1837
+ if ( yIsString && xIsNumber ) {
1838
+ return { categories : toArray ( y ) , values : toArray ( x ) } ;
1839
+ } else if ( xIsString && yIsNumber ) {
1840
+ return { categories : toArray ( x ) , values : toArray ( y ) } ;
1841
+ } else {
1842
+ return { categories : yIsString ? toArray ( y ) : toArray ( x ) , values : yIsString ? toArray ( x ) : toArray ( y ) } ;
1843
+ }
1844
+ } else {
1845
+ if ( xIsString && yIsNumber ) {
1846
+ return { categories : toArray ( x ) , values : toArray ( y ) } ;
1847
+ } else if ( yIsString && xIsNumber ) {
1848
+ return { categories : toArray ( y ) , values : toArray ( x ) } ;
1849
+ } else {
1850
+ return { categories : xIsString ? toArray ( x ) : toArray ( y ) , values : xIsString ? toArray ( y ) : toArray ( x ) } ;
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ export const transformPlotlyJsonToFunnelChartProps = (
1856
+ input : PlotlySchema ,
1857
+ isMultiPlot : boolean ,
1858
+ colorMap : React . MutableRefObject < Map < string , string > > ,
1859
+ colorwayType : ColorwayType ,
1860
+ isDarkTheme ?: boolean ,
1861
+ ) : FunnelChartProps => {
1862
+ const funnelData : FunnelChartDataPoint [ ] = [ ] ;
1863
+
1864
+ // Determine if data is stacked based on multiple series with multiple values per series
1865
+ const isStacked =
1866
+ input . data . length > 1 &&
1867
+ input . data . every ( ( series : Partial < PlotData > ) => {
1868
+ const values = series . values ?? series . x ?? series . value ;
1869
+ const labels = series . labels ?? series . y ?? series . stage ;
1870
+ return Array . isArray ( labels ) && Array . isArray ( values ) && values . length > 1 && labels . length > 1 ;
1871
+ } ) ;
1872
+
1873
+ if ( isStacked ) {
1874
+ // Assign a color per series/category and use it for all subValues of that category
1875
+ const seriesColors : Record < string , string > = { } ;
1876
+ input . data . forEach ( ( series : Partial < PlotData > , seriesIdx : number ) => {
1877
+ const category = series . name || `Category ${ seriesIdx + 1 } ` ;
1878
+ // Use the same color for this category across all stages
1879
+ const extractedColors = extractColor (
1880
+ input . layout ?. template ?. layout ?. colorway ,
1881
+ colorwayType ,
1882
+ series . marker ?. colors ?? series . marker ?. color ,
1883
+ colorMap ,
1884
+ isDarkTheme ,
1885
+ ) ;
1886
+ // Always use the first color for the series/category
1887
+ const color = resolveColor ( extractedColors , 0 , category , colorMap , isDarkTheme ) ;
1888
+ seriesColors [ category ] = color ;
1889
+
1890
+ const labels = series . labels ?? series . y ?? series . stage ;
1891
+ const values = series . values ?? series . x ?? series . value ;
1892
+
1893
+ if ( ! isArrayOrTypedArray ( labels ) || ! isArrayOrTypedArray ( values ) ) {
1894
+ return ;
1895
+ }
1896
+ if ( labels && isArrayOrTypedArray ( labels ) && labels . length > 0 ) {
1897
+ ( labels as ( string | number ) [ ] ) . forEach ( ( label : string , i : number ) => {
1898
+ const stageIndex = funnelData . findIndex ( stage => stage . stage === label ) ;
1899
+ const valueNum = Number ( ( values as ( string | number ) [ ] ) [ i ] ) ;
1900
+ if ( isNaN ( valueNum ) ) {
1901
+ return ;
1902
+ }
1903
+ if ( stageIndex === - 1 ) {
1904
+ funnelData . push ( {
1905
+ stage : label ,
1906
+ subValues : [ { category, value : valueNum , color } ] ,
1907
+ } ) ;
1908
+ } else {
1909
+ funnelData [ stageIndex ] . subValues ! . push ( { category, value : valueNum , color } ) ;
1910
+ }
1911
+ } ) ;
1912
+ }
1913
+ } ) ;
1914
+ } else {
1915
+ // Non-stacked data handling (multiple series with single-value arrays)
1916
+ input . data . forEach ( ( series : Partial < PlotData > , seriesIdx : number ) => {
1917
+ const { categories, values } = getCategoriesAndValues ( series ) ;
1918
+
1919
+ if ( ! isArrayOrTypedArray ( categories ) || ! isArrayOrTypedArray ( values ) ) {
1920
+ return ;
1921
+ }
1922
+
1923
+ const extractedColors = extractColor (
1924
+ input . layout ?. template ?. layout ?. colorway ,
1925
+ colorwayType ,
1926
+ series . marker ?. colors ?? series . marker ?. color ,
1927
+ colorMap ,
1928
+ isDarkTheme ,
1929
+ ) ;
1930
+
1931
+ categories . forEach ( ( label : string , i : number ) => {
1932
+ const color = resolveColor ( extractedColors , i , label , colorMap , isDarkTheme ) ;
1933
+ const valueNum = Number ( values [ i ] ) ;
1934
+ if ( isNaN ( valueNum ) ) {
1935
+ return ;
1936
+ }
1937
+ funnelData . push ( {
1938
+ stage : label ,
1939
+ value : valueNum ,
1940
+ color,
1941
+ } ) ;
1942
+ } ) ;
1943
+ } ) ;
1944
+ }
1945
+
1946
+ return {
1947
+ data : funnelData ,
1948
+ width : input . layout ?. width ,
1949
+ height : input . layout ?. height ,
1950
+ orientation : ( input . data [ 0 ] as Partial < PlotData > ) ?. orientation === 'v' ? 'horizontal' : 'vertical' ,
1951
+ hideLegend : isMultiPlot || input . layout ?. showlegend === false ,
1952
+ } ;
1953
+ } ;
1954
+
1734
1955
export const projectPolarToCartesian = ( input : PlotlySchema ) : PlotlySchema => {
1735
1956
const projection : PlotlySchema = { ...input } ;
1736
1957
@@ -2711,3 +2932,31 @@ const getAxisType = (data: Data[], axLetter: 'x' | 'y', ax: Partial<LayoutAxis>
2711
2932
return 'category' ;
2712
2933
}
2713
2934
} ;
2935
+
2936
+ /**
2937
+ * This is experimental. Use it only with valid datetime strings to verify if they conform to the ISO 8601 format.
2938
+ */
2939
+ const isoDateRegex = / ^ \d { 4 } ( - \d { 2 } ( - \d { 2 } ) ? ) ? ( T \d { 2 } : \d { 2 } ( : \d { 2 } ( \. \d { 1 , 9 } ) ? ) ? ( Z ) ? ) ? $ / ;
2940
+
2941
+ /**
2942
+ * We want to display localized date and time in the charts, so the useUTC prop is set to false.
2943
+ * But this can sometimes cause the formatters to display the datetime incorrectly.
2944
+ * To work around this issue, we use this function to adjust datetime strings so that they are always interpreted
2945
+ * as local time, allowing the formatters to produce the correct output.
2946
+ *
2947
+ * FIXME: The formatters should always produce a clear and accurate localized output, regardless of the
2948
+ * format used to create the date object.
2949
+ */
2950
+ const parseLocalDate = ( value : string | number ) => {
2951
+ if ( typeof value === 'string' ) {
2952
+ const match = value . match ( isoDateRegex ) ;
2953
+ if ( match ) {
2954
+ if ( ! match [ 3 ] ) {
2955
+ value += 'T00:00' ;
2956
+ } else if ( match [ 6 ] ) {
2957
+ value = value . replace ( 'Z' , '' ) ;
2958
+ }
2959
+ }
2960
+ }
2961
+ return new Date ( value ) ;
2962
+ } ;
0 commit comments