1212// limitations under the License.
1313
1414import { ReactElement , useMemo } from 'react' ;
15- import { FormatOptions , TimeScale } from '@perses-dev/core' ;
16- import { EChart , getFormattedAxis , useChartsTheme , useTimeZone } from '@perses-dev/components' ;
15+ import { formatValue , FormatOptions , TimeScale } from '@perses-dev/core' ;
16+ import { EChart , useChartsTheme , useTimeZone } from '@perses-dev/components' ;
1717import { use , EChartsCoreOption } from 'echarts/core' ;
18- import { HeatmapChart as EChartsHeatmapChart } from 'echarts/charts' ;
18+ import { CustomChart } from 'echarts/charts' ;
19+ import type { CustomSeriesRenderItemAPI , CustomSeriesRenderItemParams } from 'echarts' ;
1920import { useTheme } from '@mui/material' ;
21+ import { LOG_BASE } from '../heat-map-chart-model' ;
2022import { getFormattedHeatmapAxisLabel } from '../utils' ;
2123import { generateTooltipHTML } from './HeatMapTooltip' ;
2224
23- use ( [ EChartsHeatmapChart ] ) ;
25+ use ( [ CustomChart ] ) ;
2426
2527// The default coloring is a blue->yellow->red gradient
2628const DEFAULT_VISUAL_MAP_COLORS = [
@@ -37,7 +39,7 @@ const DEFAULT_VISUAL_MAP_COLORS = [
3739 '#a50026' ,
3840] ;
3941
40- export type HeatMapData = [ number , number , number | undefined ] ; // [x, y, value ]
42+ export type HeatMapData = [ number , number , number , number | undefined ] ; // [xIndex, yLower, yUpper, count ]
4143
4244export interface HeatMapDataItem {
4345 value : HeatMapData ;
@@ -54,28 +56,31 @@ export interface HeatMapChartProps {
5456 height : number ;
5557 data : HeatMapDataItem [ ] ;
5658 xAxisCategories : number [ ] ;
57- yAxisCategories : string [ ] ;
5859 yAxisFormat ?: FormatOptions ;
5960 countFormat ?: FormatOptions ;
6061 countMin ?: number ;
6162 countMax ?: number ;
6263 timeScale ?: TimeScale ;
6364 showVisualMap ?: boolean ;
64- // TODO: exponential?: boolean;
65+ min ?: number ;
66+ max ?: number ;
67+ logBase ?: LOG_BASE ;
6568}
6669
6770export function HeatMapChart ( {
6871 width,
6972 height,
7073 data,
7174 xAxisCategories,
72- yAxisCategories,
7375 yAxisFormat,
7476 countFormat,
7577 countMin,
7678 countMax,
7779 timeScale,
7880 showVisualMap,
81+ min,
82+ max,
83+ logBase,
7984} : HeatMapChartProps ) : ReactElement | null {
8085 const chartsTheme = useChartsTheme ( ) ;
8186 const theme = useTheme ( ) ;
@@ -91,7 +96,6 @@ export function HeatMapChart({
9196 label : params . data . label ,
9297 marker : params . marker ,
9398 xAxisCategories,
94- yAxisCategories,
9599 theme,
96100 yAxisFormat : yAxisFormat ,
97101 countFormat : countFormat ,
@@ -106,13 +110,24 @@ export function HeatMapChart({
106110 formatter : getFormattedHeatmapAxisLabel ( timeScale ?. rangeMs ?? 0 , timeZone ) ,
107111 } ,
108112 } ,
109- yAxis : getFormattedAxis (
110- {
111- type : 'category' ,
112- data : yAxisCategories ,
113+ yAxis : {
114+ type : logBase !== undefined ? 'log' : 'value' ,
115+ logBase : logBase ,
116+ boundaryGap : [ 0 , '10%' ] ,
117+ min : min ,
118+ max : max ,
119+ axisLabel : {
120+ hideOverlap : true ,
121+ formatter : ( value : number ) : string => {
122+ // On log scales, ECharts may generate a tick at 0 which is mathematically
123+ // invalid (log(0) is undefined). Return empty string to hide such labels.
124+ if ( logBase !== undefined && value === 0 ) {
125+ return '' ;
126+ }
127+ return formatValue ( value , yAxisFormat ) ;
128+ } ,
113129 } ,
114- yAxisFormat
115- ) ,
130+ } ,
116131 visualMap : {
117132 show : showVisualMap ?? false ,
118133 type : 'continuous' ,
@@ -132,18 +147,49 @@ export function HeatMapChart({
132147 textBorderColor : theme . palette . background . default ,
133148 textBorderWidth : 5 ,
134149 } ,
150+ // Color by the count dimension (index 3)
151+ dimension : 3 ,
135152 } ,
136153 series : [
137154 {
138- name : 'Gaussian' ,
139- type : 'heatmap' ,
140- data : data ,
141- emphasis : {
142- itemStyle : {
143- borderColor : '#333' ,
144- borderWidth : 1 ,
145- } ,
155+ name : 'HeatMap' ,
156+ type : 'custom' ,
157+ renderItem : function ( params : CustomSeriesRenderItemParams , api : CustomSeriesRenderItemAPI ) {
158+ const xIndex = api . value ( 0 ) as number ;
159+ const yLower = api . value ( 1 ) as number ;
160+ const yUpper = api . value ( 2 ) as number ;
161+
162+ // Pixel coordinates
163+ const upperStart = api . coord ( [ xIndex , yUpper ] ) ;
164+ const lowerStart = api . coord ( [ xIndex , yLower ] ) ;
165+ const upperNext = api . coord ( [ xIndex + 1 , yUpper ] ) ;
166+
167+ const startX = upperStart ?. [ 0 ] ;
168+ const upperY = upperStart ?. [ 1 ] ;
169+ const lowerY = lowerStart ?. [ 1 ] ;
170+ const nextX = upperNext ?. [ 0 ] ;
171+
172+ if ( startX === undefined || upperY === undefined || lowerY === undefined || nextX === undefined ) {
173+ return null ;
174+ }
175+
176+ const topY = Math . min ( upperY , lowerY ) ;
177+ const bottomY = Math . max ( upperY , lowerY ) ;
178+ const width = nextX - startX ;
179+ const height = bottomY - topY ;
180+
181+ return {
182+ type : 'rect' ,
183+ shape : { x : startX , y : topY , width, height } ,
184+ style : {
185+ fill : api . visual ( 'color' ) ,
186+ } ,
187+ } ;
146188 } ,
189+ label : { show : false } ,
190+ dimensions : [ 'xIndex' , 'yLower' , 'yUpper' , 'count' ] ,
191+ encode : { x : 0 , y : [ 1 , 2 ] , tooltip : [ 1 , 2 , 3 ] } ,
192+ data : data ,
147193 progressive : 1000 ,
148194 animation : false ,
149195 } ,
@@ -153,7 +199,6 @@ export function HeatMapChart({
153199 xAxisCategories ,
154200 timeScale ?. rangeMs ,
155201 timeZone ,
156- yAxisCategories ,
157202 yAxisFormat ,
158203 showVisualMap ,
159204 countMin ,
@@ -162,6 +207,9 @@ export function HeatMapChart({
162207 theme ,
163208 data ,
164209 countFormat ,
210+ min ,
211+ max ,
212+ logBase ,
165213 ] ) ;
166214
167215 const chart = useMemo (
0 commit comments