import{useResizeObserver,useThrottleFn}from"@vueuse/core";importPlotly,{typePlotlyHTMLElement,typePlotMouseEvent,typePlotRelayoutEvent,typePlotSelectionEvent,}from"plotly.js-dist-min";importtype{FilterActions}from"src/store";import{categoricalChartMargin,colorscale,defaultChartMargin,defaultMarkerSize,gridcolor,hoverlabel,labelFontSize,modebar,portMaxAllowed,portMinAllowed,tickfontcolor,}from"utils/chart";importtype{ChartDataPoint}from"utils/filters";import{onBeforeUnmount,ref,toValue,typeMaybeRefOrGetter,typeRef,}from"vue";exportfunctionusePlotlyChart(chartContainer: Ref<HTMLDivElement|undefined>,chartData: MaybeRefOrGetter<ChartDataPoint[]>,options?:{yField?: keyofChartDataPoint;yTitle?: string;categoricalY?: boolean;onIPClick?:(ip: string,event: PlotMouseEvent)=>void;onSelected?:(event: PlotSelectionEvent|undefined)=>void;onRelayout?:(event: PlotRelayoutEvent)=>void;xRange?: string[];yRange?: string[];},){constrelayout=ref(false);constallColors=ref<number[]>([]);constallRemoteAddrs=ref<string[]>([]);constallPorts=ref<number[]>([]);constdebouncedDrawPlot=useThrottleFn(()=>drawPlot(),250);onBeforeUnmount(()=>{if(chartContainer.value){Plotly.purge(chartContainer.value);}});functiondrawPlot(forceNewPlot: boolean=false){if(!chartContainer.value)return;constdata=toValue(chartData);constx: Date[]=[];consty: any[]=[];constcolors: number[]=[];constcustomdata: Array<[string,string,number,string]>=[];constremoteAddrs: string[]=[];constports: number[]=[];constyField=options?.yField||"dst_port";data.forEach((point: ChartDataPoint)=>{x.push(point.time);y.push(point[yField]);colors.push(point.remote_addr_int);remoteAddrs.push(point.remote_addr);ports.push(point.dst_port);customdata.push([point.time_with_ms,point.remote_addr,point.dst_port,point.event??"",]);});allColors.value=colors.slice();allRemoteAddrs.value=remoteAddrs.slice();allPorts.value=ports.slice();consttraces: Plotly.Data[]=[{type:"scattergl"asconst,mode:"markers"asconst,x: x,y: y,marker:{size: defaultMarkerSize,color: colors.slice(),showscale: false,colorscale: colorscale,},hovertemplate:"Time: %{customdata[0]}<br>"+"IP: %{customdata[1]}<br>"+"Port: %{customdata[2]}<br>"+"Event: %{customdata[3]}<br>"+"<extra></extra>",customdata: customdata,},];constlayout: Partial<Plotly.Layout>={xaxis:{title:{text:"Time",font:{color: tickfontcolor,size: labelFontSize},},type:"date"asconst,gridcolor: gridcolor,range: options?.xRange?.length===2?options.xRange : undefined,tickfont:{color: tickfontcolor,size: labelFontSize},},yaxis:{title:{text: options?.yTitle||"Port",font:{color: tickfontcolor,size: labelFontSize},},gridcolor: gridcolor,range: options?.yRange?.length===2?options.yRange : undefined,tickfont:{color: tickfontcolor,size: labelFontSize},},hovermode:"closest"asconst,margin: options?.categoricalY?categoricalChartMargin : defaultChartMargin,plot_bgcolor:"transparent",paper_bgcolor:"transparent",hoverlabel: hoverlabel,modebar: modebar,};if(!layout.yaxis){layout.yaxis={};}if(options?.categoricalY){layout.yaxis.type="category";// Sort unique Y values to keep the axis consistent
constuniqueY=[...newSet(y)].sort();layout.yaxis.categoryorder="array";layout.yaxis.categoryarray=uniqueY;}else{layout.yaxis.tickformat=" ";layout.yaxis.maxallowed=portMaxAllowed;layout.yaxis.minallowed=portMinAllowed;}constconfig: Partial<Plotly.Config>={responsive: true,scrollZoom: true,displaylogo: false,};if(relayout.value&&!forceNewPlot){constgd=chartContainer.valueasany;constxRange=gd.layout.xaxis.range;constyRange=gd.layout.yaxis.range;constnewLayout: Partial<Plotly.Layout>={...gd.layout,};if(gd.layout.xaxis.autorange===false){newLayout.xaxis={...gd.layout.xaxis,range: xRange};}if(gd.layout.yaxis.autorange===false){newLayout.yaxis={...gd.layout.yaxis,range: yRange};}Plotly.react(chartContainer.value,traces,newLayout,config);}else{relayout.value=true;constplotDiv=chartContainer.valueasunknownasPlotlyHTMLElement;Plotly.newPlot(chartContainer.value,traces,layout,config);plotDiv.on("plotly_click",(eventData)=>{constclickedIdx=eventData.points[0]?.pointIndex;constclickedIP=clickedIdx?allRemoteAddrs.value[clickedIdx]:undefined;if(clickedIP){options?.onIPClick?.(clickedIP,eventData);}});plotDiv.on("plotly_relayout",(eventData)=>{options?.onRelayout?.(eventData);});plotDiv.on("plotly_selected",(eventData: PlotSelectionEvent|undefined)=>{options?.onSelected?.(eventData);},);}}useResizeObserver(chartContainer,()=>{if(chartContainer.value&&relayout.value){Plotly.Plots.resize(chartContainer.value);}});return{drawPlot,debouncedDrawPlot,allColors,allRemoteAddrs,allPorts,};}exportfunctionsetRangeToFilter(event: PlotRelayoutEvent,filterActions: FilterActions,){letyRange: number[]|undefined;// Plotly relayout events can have a nested structure or flat keys
if(event.yaxis?.range){yRange=event.yaxis.range;}elseif(event["yaxis.range[0]"]!==undefined&&event["yaxis.range[1]"]!==undefined){yRange=[event["yaxis.range[0]"],event["yaxis.range[1]"]];}if(yRange?.[0]&&yRange?.[1]){conststart=Math.max(Math.round(yRange[0]),0);constend=Math.min(Math.round(yRange[1]),65535);// Use setDstPort to replace the filter with the current zoom range
filterActions.setDstPort(`${start}-${end}`);}elseif(event["yaxis.autorange"]||event.yaxis?.autorange){// If the user resets zoom (e.g. double click), clear the port filter
filterActions.state.dst_port=[];}letxRange: number[]|undefined;if(event.xaxis?.range){xRange=event.xaxis.range;}elseif(event["xaxis.range[0]"]!==undefined&&event["xaxis.range[1]"]!==undefined){xRange=[event["xaxis.range[0]"],event["xaxis.range[1]"]];}if(xRange?.[0]&&xRange?.[1]){// Plotly uses ISO 8601 in local timezone, so we need to convert to UTC and then slice off the timezone part
letstart=newDate(xRange[0]);letend=newDate(xRange[1]);// add utc offset to start and end
start.setUTCMinutes(start.getUTCMinutes()-start.getTimezoneOffset());end.setUTCMinutes(end.getUTCMinutes()-end.getTimezoneOffset());// round start down to last second and round end up to next second
start.setMilliseconds(0);end.setMilliseconds(0);end.setSeconds(end.getSeconds()+1);filterActions.setTimeStart(start.toISOString().slice(0,19));filterActions.setTimeEnd(end.toISOString().slice(0,19));}}