1
+ import * as dfns from 'date-fns'
1
2
import * as echarts from 'echarts-unofficial-v6'
2
3
import { fontFamily as twFontFamiliy } from 'tailwindcss/defaultTheme'
3
4
import { format as d3Format } from 'd3-format'
@@ -12,7 +13,7 @@ import {
12
13
} from '@heroicons/react/20/solid'
13
14
import { CubeTransparentIcon } from '@heroicons/react/24/solid'
14
15
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
15
- import { DataFrame } from '@briefer/types'
16
+ import { DataFrame , TimeUnit } from '@briefer/types'
16
17
import LargeSpinner from '@/components/LargeSpinner'
17
18
import clsx from 'clsx'
18
19
import useSideBar from '@/hooks/useSideBar'
@@ -23,6 +24,7 @@ import { findMaxFontSize, measureText } from '@/measureText'
23
24
import {
24
25
VisualizationV2BlockInput ,
25
26
VisualizationV2BlockOutputResult ,
27
+ XAxis ,
26
28
} from '@briefer/editor'
27
29
import { head } from 'ramda'
28
30
@@ -269,16 +271,16 @@ function BrieferResult(props: {
269
271
left : ! props . hasControls ? 18 : 'center' ,
270
272
} ,
271
273
grid,
272
- xAxis : props . result . xAxis . map ( ( axis ) => ( {
273
- ... axis ,
274
- axisLabel : {
275
- hideOverlap : true ,
276
- interval : axis . type === 'category' ? 0 : 'auto' ,
277
- } ,
278
- splitLine : {
279
- show : false ,
280
- } ,
281
- } ) ) ,
274
+ xAxis : props . result . xAxis . map ( ( axis ) => {
275
+ switch ( axis . type ) {
276
+ case 'value' :
277
+ return getValueAxis ( axis , props . result )
278
+ case 'time' :
279
+ return getTimeAxis ( axis , props . result )
280
+ case 'category' :
281
+ return getCategoryAxis ( axis )
282
+ }
283
+ } ) ,
282
284
yAxis : props . result . yAxis . map ( ( axis ) => ( {
283
285
...axis ,
284
286
} ) ) ,
@@ -337,7 +339,7 @@ function Echarts(props: EchartsProps) {
337
339
const hiddenChart = echarts . init ( hiddenRef . current , null , {
338
340
renderer : 'svg' ,
339
341
} )
340
- hiddenChart . setOption ( {
342
+ const hiddenChartOption = {
341
343
...props . option ,
342
344
// set animation to be as fast as possible, since finished event does not get fired when no animation
343
345
animationDelay : 0 ,
@@ -354,7 +356,9 @@ function Echarts(props: EchartsProps) {
354
356
hideOverlap : false ,
355
357
} ,
356
358
} ) ) ,
357
- } )
359
+ }
360
+
361
+ hiddenChart . setOption ( hiddenChartOption )
358
362
359
363
const handleFinished = ( ) => {
360
364
const xAxes = Array . isArray ( props . option . xAxis )
@@ -662,4 +666,260 @@ function BigNumberVisualization(props: {
662
666
}
663
667
}
664
668
669
+ function getValuesMinInterval ( xValues : number [ ] ) : number {
670
+ if ( xValues . length === 0 ) {
671
+ return 0
672
+ }
673
+
674
+ let minDiff = Infinity
675
+
676
+ for ( let i = 0 ; i < xValues . length - 1 ; i ++ ) {
677
+ const a = xValues [ i ]
678
+ const b = xValues [ i + 1 ]
679
+ if ( a === b ) {
680
+ continue
681
+ }
682
+
683
+ const diff = Math . abs ( a - b )
684
+ if ( diff < minDiff ) {
685
+ minDiff = diff
686
+ }
687
+ }
688
+
689
+ return minDiff
690
+ }
691
+
692
+ function getValueAxis ( axis : XAxis , result : VisualizationV2BlockOutputResult ) {
693
+ let interval : 'auto' | number = 'auto'
694
+
695
+ let min = - Infinity
696
+ let max = Infinity
697
+ const xFields = result . series
698
+ . map ( ( s ) => s . encode ?. x )
699
+ . filter ( ( x ) : x is string | number => x !== undefined )
700
+ const values = result . dataset
701
+ . flatMap ( ( d ) =>
702
+ xFields . flatMap ( ( f ) => d . source . flatMap ( ( r ) => r [ f . toString ( ) ] ) )
703
+ )
704
+ . filter ( ( v ) => typeof v === 'number' )
705
+
706
+ if ( values . length > 0 ) {
707
+ interval = getValuesMinInterval ( values )
708
+ min = Math . min ( ...values )
709
+ max = Math . max ( ...values )
710
+ }
711
+
712
+ return {
713
+ ...axis ,
714
+ axisLabel : {
715
+ margin : 5 ,
716
+ hideOverlap : true ,
717
+ ...( typeof interval === 'number'
718
+ ? {
719
+ formatter : ( value : string | number ) : string => {
720
+ if ( typeof value === 'number' && ( value < min || value > max ) ) {
721
+ return ''
722
+ }
723
+
724
+ return value . toString ( )
725
+ } ,
726
+ }
727
+ : { } ) ,
728
+ } ,
729
+ min : interval !== 'auto' ? min - interval / 2 : 'dataMin' ,
730
+ max : interval !== 'auto' ? max + interval / 2 : 'dataMax' ,
731
+ ...( interval !== 'auto'
732
+ ? {
733
+ minInterval : interval ,
734
+ }
735
+ : { } ) ,
736
+ splitLine : {
737
+ show : false ,
738
+ } ,
739
+ }
740
+ }
741
+
742
+ function getTimeAxis ( axis : XAxis , result : VisualizationV2BlockOutputResult ) {
743
+ const intervalOrder : {
744
+ [ T in TimeUnit ] : number
745
+ } = {
746
+ seconds : 0 ,
747
+ minutes : 1 ,
748
+ hours : 2 ,
749
+ date : 3 ,
750
+ week : 4 ,
751
+ month : 5 ,
752
+ quarter : 6 ,
753
+ year : 7 ,
754
+ }
755
+
756
+ const xFields = result . series
757
+ . map ( ( s ) => s . encode ?. x )
758
+ . filter ( ( x ) : x is string | number => x !== undefined )
759
+ const values = result . dataset
760
+ . flatMap ( ( d ) =>
761
+ xFields . flatMap ( ( f ) => d . source . flatMap ( ( r ) => new Date ( r [ f . toString ( ) ] ) ) )
762
+ )
763
+ . filter ( ( date ) => dfns . isValid ( date ) )
764
+
765
+ let min = values [ 0 ]
766
+ let max = values [ 0 ]
767
+ let minIntervalUnit : TimeUnit = 'year'
768
+ for ( let i = 0 ; i < values . length - 1 ; i ++ ) {
769
+ const a = values [ i ]
770
+ const b = values [ i + 1 ]
771
+ if ( ! a || ! b || ! dfns . isValid ( a ) || ! dfns . isValid ( b ) ) {
772
+ continue
773
+ }
774
+
775
+ const intervalUnit = getIntervalUnit ( a , b )
776
+
777
+ if ( intervalOrder [ intervalUnit ] < intervalOrder [ minIntervalUnit ] ) {
778
+ minIntervalUnit = intervalUnit
779
+ }
780
+
781
+ min =
782
+ a . getTime ( ) < min . getTime ( ) ? a : b . getTime ( ) < min . getTime ( ) ? b : min
783
+ max =
784
+ a . getTime ( ) > max . getTime ( ) ? a : b . getTime ( ) > max . getTime ( ) ? b : max
785
+ }
786
+
787
+ const hideDay = values . every ( ( v ) => {
788
+ switch ( minIntervalUnit ) {
789
+ case 'year' :
790
+ return dfns . getDate ( v ) === 1 && dfns . getMonth ( v ) === 0
791
+ case 'quarter' :
792
+ return dfns . getDate ( v ) === 1 && dfns . getMonth ( v ) % 3 === 0
793
+ case 'month' :
794
+ return dfns . isFirstDayOfMonth ( v )
795
+ case 'week' :
796
+ case 'date' :
797
+ case 'hours' :
798
+ case 'minutes' :
799
+ case 'seconds' :
800
+ return false
801
+ }
802
+ } )
803
+
804
+ const minInterval : number = ( ( ) => {
805
+ switch ( minIntervalUnit ) {
806
+ case 'year' :
807
+ return 365 * 24 * 60 * 60 * 1000
808
+ case 'quarter' :
809
+ return 91 * 24 * 60 * 60 * 1000
810
+ case 'month' :
811
+ return 30 * 24 * 60 * 60 * 1000
812
+ case 'week' :
813
+ return 7 * 24 * 60 * 60 * 1000
814
+ case 'date' :
815
+ return 24 * 60 * 60 * 1000
816
+ case 'hours' :
817
+ return 60 * 60 * 1000
818
+ case 'minutes' :
819
+ return 60 * 1000
820
+ case 'seconds' :
821
+ return 1000
822
+ }
823
+ } ) ( )
824
+
825
+ return {
826
+ ...axis ,
827
+ axisLabel : {
828
+ margin : 5 ,
829
+ hideOverlap : true ,
830
+ formatter : ( value : string | number ) : string => {
831
+ const asDate = new Date ( value )
832
+ if ( asDate < min || asDate > max ) {
833
+ return ''
834
+ }
835
+
836
+ switch ( minIntervalUnit ) {
837
+ case 'year' :
838
+ if ( hideDay ) {
839
+ return dfns . format ( value , 'yyyy' )
840
+ } else {
841
+ return dfns . format ( value , 'MMMM d, yyyy' )
842
+ }
843
+ case 'quarter' :
844
+ case 'month' :
845
+ case 'week' :
846
+ case 'date' :
847
+ if ( hideDay ) {
848
+ return dfns . format ( value , 'MMMM yyyy' )
849
+ } else {
850
+ return dfns . format ( value , 'MMMM d, yyyy' )
851
+ }
852
+ case 'hours' :
853
+ case 'minutes' :
854
+ return dfns . format ( value , 'MMMM d, yyyy, h:mm a' )
855
+ case 'seconds' :
856
+ return dfns . format ( value , 'MMMM d, yyyy, h:mm:ss a' )
857
+ }
858
+ } ,
859
+ showMaxLabel : true ,
860
+ showMinLabel : true ,
861
+ } ,
862
+ axisTick : {
863
+ show : false ,
864
+ } ,
865
+ min : min . getTime ( ) - minInterval / 2 ,
866
+ max : max . getTime ( ) + minInterval / 2 ,
867
+ minInterval,
868
+ splitLine : {
869
+ show : false ,
870
+ } ,
871
+ }
872
+ }
873
+
874
+ function getIntervalUnit ( a : Date , b : Date ) : TimeUnit {
875
+ const years = Math . abs ( dfns . differenceInYears ( b , a ) )
876
+ if ( years >= 1 ) {
877
+ return 'year'
878
+ }
879
+
880
+ const months = Math . abs ( dfns . differenceInMonths ( b , a ) )
881
+ if ( months >= 3 ) {
882
+ return 'quarter'
883
+ }
884
+
885
+ if ( months >= 1 ) {
886
+ return 'month'
887
+ }
888
+
889
+ const weeks = Math . abs ( dfns . differenceInWeeks ( b , a ) )
890
+ if ( weeks >= 1 ) {
891
+ return 'week'
892
+ }
893
+
894
+ const days = Math . abs ( dfns . differenceInDays ( b , a ) )
895
+ if ( days >= 1 ) {
896
+ return 'date'
897
+ }
898
+
899
+ const hours = Math . abs ( dfns . differenceInHours ( b , a ) )
900
+ if ( hours >= 1 ) {
901
+ return 'hours'
902
+ }
903
+
904
+ const minutes = Math . abs ( dfns . differenceInMinutes ( b , a ) )
905
+ if ( minutes >= 1 ) {
906
+ return 'minutes'
907
+ }
908
+
909
+ return 'seconds'
910
+ }
911
+
912
+ function getCategoryAxis ( axis : XAxis ) {
913
+ return {
914
+ ...axis ,
915
+ axisLabel : {
916
+ hideOverlap : true ,
917
+ interval : 0 ,
918
+ } ,
919
+ splitLine : {
920
+ show : false ,
921
+ } ,
922
+ }
923
+ }
924
+
665
925
export default VisualizationViewV2
0 commit comments