-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Utilization] Render Timeseries Component (#6255)
# Description Add customized LineRectChart(TimeSeries+ TimeSegment) chart for Utilization. the main function to review is LineRectChart.tsx in torchci/components/charts/line_rect_chart ## Why not use existing echarts? Existing chart options such as apache echart has limits to provide what this feature want to achieve: - display timeseries data (line) and segments (rectangular) many echart product support combo visualization but the closest one is `bar chart + line chart`, which cannot fulfil our needs. - complicated interaction with component out of the charts ## Why D3.js D3.js is the super powerful svg rendering library that most existing echart product used to create charts from scratch. It's common library tools, and most echart product is a wrapper of it ## is this one time thing? This time series + rec chart can be a good base if we want to have more customized echart for our visualization. many component I created here is reusable, for instance, if we want customized line filter options (instead of the echart's general legend one which does not fit UI when there are many lines) ## About LineRectChart LineRectChart contains: - Svg elements: line, rectangulars, circle tooltip, vertical dash line tooltip - React checkbox list to group time series based on category "hardware" and "stats" - React tooltips for lines and rectangulars LineRectChart functionality: - visualize time series based on group option: "hardware" and "statistics(stats)" - visualize test segments - display tooltip for test segment (name, starttime and endtime) - display tooltip, mouse position (dashed line) for time series - disable test segment - disable time series tooltip ## D3 vs Native React manage most of the logic using React Approach (useState etc), but for complicated rendering such as Axis and mouseOn rendering for tooltips, we use d3.js which is directly applying changes to DOM element, To learn more about d3 https://d3js.org/ ## Demo the feature: live demo: https://torchci-kbu4nenyq-fbopensource.vercel.app/utilization/12937937547/36088234580/1 ### Visualize time series line with tooltip and dashline:  ### Choose different option to enable and disable time series lines  ### visualize the segment for test and its tooltip 
- Loading branch information
Showing
23 changed files
with
1,615 additions
and
51 deletions.
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
torchci/components/charts/line_rect_chart/LineChart.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.chartContainer { | ||
display: flex; /* or inline-flex */ | ||
} |
177 changes: 177 additions & 0 deletions
177
torchci/components/charts/line_rect_chart/LineRectChart.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import * as d3 from "d3"; | ||
import { TimeSeriesWrapper } from "lib/utilization/types"; | ||
import { useEffect, useRef, useState } from "react"; | ||
import { TooltipElement } from "./component/helpers/Tooltip"; | ||
import RenderLinePickerOptions from "./component/RenderLinePickerOptions"; | ||
import RenderSvgLines from "./component/RenderSvgLine"; | ||
import RenderSvgLineTooltipElements from "./component/RenderSvgLineTooltipElements"; | ||
import RenderSvgRects from "./component/RenderSvgRect"; | ||
import { D3LineRecord, Line, PickerConfig, RectangleData } from "./lib/types"; | ||
import { processLineData, processRectData, setDimensions } from "./lib/utils"; | ||
import styles from "./LineChart.module.css"; | ||
|
||
type Props = { | ||
onDataChange?: (data: any) => void; | ||
inputLines?: TimeSeriesWrapper[]; | ||
rects?: { | ||
name: string; | ||
start_at: string; | ||
end_at: string; | ||
color?: string; | ||
}[]; | ||
chartWidth?: number; | ||
disableRect?: boolean; | ||
disableLineTooltip?: boolean; | ||
selectedLineId?: string; | ||
lineFilterConfig?: PickerConfig[]; | ||
}; | ||
|
||
const LineRectChart = ({ | ||
onDataChange = (data: any) => void {}, | ||
inputLines, | ||
rects, | ||
chartWidth, | ||
disableRect = false, | ||
disableLineTooltip = false, | ||
lineFilterConfig, | ||
}: Props) => { | ||
const dimensions = setDimensions(chartWidth); | ||
|
||
// svg element state | ||
const svgRef = useRef<SVGSVGElement | null>(null); | ||
// d3 scales state | ||
const [scales, setScales] = useState<{ xScale: any; yScale: any }>({ | ||
xScale: null, | ||
yScale: null, | ||
}); | ||
// line and rect states | ||
const [lines, setLines] = useState<Line[]>([]); | ||
const [lineConfigs, setLineConfigs] = useState< | ||
{ name: string; id: string; hidden: boolean }[] | ||
>([]); | ||
const [rectangles, setRectangles] = useState<RectangleData[]>([]); | ||
|
||
// tooltip state | ||
const [lineTooltip, setLineTooltip] = useState<{ | ||
visible: boolean; | ||
content: any; | ||
position: { x: number; y: number }; | ||
}>({ visible: false, content: null, position: { x: 0, y: 0 } }); | ||
const [rectTooltip, setRectTooltip] = useState<{ | ||
visible: boolean; | ||
content: any; | ||
position: { x: number; y: number }; | ||
}>({ visible: false, content: null, position: { x: 0, y: 0 } }); | ||
|
||
useEffect(() => { | ||
let lineData: Line[] = []; | ||
if (inputLines) { | ||
lineData = processLineData(inputLines); | ||
setLines(lineData); | ||
setLineConfigs( | ||
lineData.map((line) => { | ||
return { name: line.name, id: line.id, hidden: false }; | ||
}) | ||
); | ||
} | ||
|
||
if (rects) { | ||
let recs = processRectData(rects); | ||
setRectangles(recs); | ||
} | ||
|
||
if (lineData.length > 0) { | ||
// set x axis for svg | ||
const xScale = d3 | ||
.scaleTime() | ||
.domain( | ||
d3.extent(lineData[0].records, (d: D3LineRecord) => d.date) as [ | ||
Date, | ||
Date | ||
] | ||
) | ||
.range([0, dimensions.ctrWidth]); | ||
|
||
// Set y axis scale for svg | ||
const yScale = d3 | ||
.scaleLinear() | ||
.domain([0, 100]) | ||
.range([dimensions.ctrHeight, 0]) | ||
.nice(); | ||
setScales({ xScale, yScale }); | ||
} | ||
return () => {}; | ||
}, [inputLines, rects]); | ||
|
||
useEffect(() => { | ||
// only render svg axis when dom is ready. | ||
if (svgRef.current && scales.xScale && scales.yScale) { | ||
const container = d3.select(svgRef.current).select(".container"); | ||
const xAxis = d3.axisBottom(scales.xScale); | ||
const yAxis = d3.axisLeft(scales.yScale); | ||
|
||
container.select(".xAxis").call(xAxis as any); | ||
container.select(".yAxis").call(yAxis as any); | ||
} | ||
}, [scales]); | ||
|
||
// handle line events | ||
return ( | ||
<div className={styles.chartContainer}> | ||
<div> | ||
<svg ref={svgRef} width={dimensions.width} height={dimensions.height}> | ||
<g | ||
className="container" | ||
transform={`translate(${dimensions.margins}, ${dimensions.margins})`} | ||
> | ||
<g | ||
className="xAxis" | ||
transform={`translate(0,${dimensions.ctrHeight})`} | ||
/> | ||
<g className="yAxis" /> | ||
<RenderSvgLines | ||
scales={scales} | ||
lines={lines} | ||
lineConfigs={lineConfigs} | ||
/> | ||
<RenderSvgLineTooltipElements | ||
lines={lines} | ||
lineConfigs={lineConfigs} | ||
dimensions={dimensions} | ||
scales={scales} | ||
container={d3.select(svgRef.current).select(".container")} | ||
disableLineTooltip={disableLineTooltip} | ||
setLineTooltip={setLineTooltip} | ||
/> | ||
<RenderSvgRects | ||
setRectTooltip={setRectTooltip} | ||
rectangles={rectangles} | ||
disableRect={disableRect} | ||
dimensions={dimensions} | ||
scales={scales} | ||
/> | ||
</g> | ||
</svg> | ||
<TooltipElement | ||
isVisible={lineTooltip.visible} | ||
content={lineTooltip.content} | ||
position={lineTooltip.position} | ||
/> | ||
<TooltipElement | ||
isVisible={rectTooltip.visible} | ||
content={rectTooltip.content} | ||
position={rectTooltip.position} | ||
/> | ||
</div> | ||
{lineFilterConfig && ( | ||
<RenderLinePickerOptions | ||
lines={lineConfigs} | ||
setLines={setLineConfigs} | ||
lineFilterConfig={lineFilterConfig} | ||
/> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default LineRectChart; |
36 changes: 36 additions & 0 deletions
36
torchci/components/charts/line_rect_chart/component/RenderLineChartComponents.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
.rowFlexCenter { | ||
display: flex; /* or inline-flex */ | ||
align-items: center; | ||
} | ||
|
||
.tooltipline { | ||
font-family: Arial, Helvetica, sans-serif; | ||
line-height: 1; | ||
padding: 12px; | ||
background-color: white; | ||
border-radius: 5px; | ||
border: 1px solid grey; | ||
} | ||
|
||
.selected-rect { | ||
fill: brown; | ||
opacity: 0.7; | ||
} | ||
|
||
.rect { | ||
opacity: 0.2; | ||
} | ||
|
||
.rect:hover { | ||
fill: blue; | ||
opacity: 0.5; | ||
} | ||
.rect:active { | ||
fill: red; | ||
} | ||
|
||
.linePickerGroup { | ||
height: 300px; /* fixed height */ | ||
overflow-y: auto; | ||
scrollbar-width: thin; | ||
} |
113 changes: 113 additions & 0 deletions
113
torchci/components/charts/line_rect_chart/component/RenderLinePickerOptions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { useEffect, useState } from "react"; | ||
import { | ||
containsAllSubstrings, | ||
PickerConfig, | ||
PickerConfigType, | ||
} from "../lib/types"; | ||
import ChartCheckboxGroups from "./helpers/ChartCheckboxGroups"; | ||
import { CheckboxItem } from "./helpers/CheckboxGroup"; | ||
import DropList from "./helpers/DropList"; | ||
import styles from "./RenderLineChartComponents.module.css"; | ||
|
||
const RenderLinePickerOptions = ({ | ||
lines, | ||
setLines, | ||
lineFilterConfig, | ||
}: { | ||
lines: { name: string; id: string; hidden: boolean }[]; | ||
setLines: (line: any[]) => void; | ||
lineFilterConfig: PickerConfig[]; | ||
}) => { | ||
const [category, setCategory] = useState<string>(""); | ||
const [options, setOptions] = useState<any>([]); | ||
const [groups, setGroups] = useState<any>([]); | ||
|
||
useEffect(() => { | ||
render(); | ||
}, [lines, lineFilterConfig]); | ||
|
||
function render() { | ||
let options = lineFilterConfig.map((config) => { | ||
return { value: config.category, name: config.category }; | ||
}); | ||
setOptions(options); | ||
|
||
const config = lineFilterConfig.find((item) => item.category == category); | ||
if (!config) { | ||
setGroups([]); | ||
return; | ||
} | ||
const res = config.types.map((type) => { | ||
return { | ||
parentName: type.name, | ||
childGroup: getChildGroup(type, lines), | ||
}; | ||
}); | ||
setGroups(res); | ||
} | ||
|
||
function resetLines() { | ||
const newLines = lines.map((line) => { | ||
line.hidden = true; | ||
return line; | ||
}); | ||
setLines(newLines); | ||
} | ||
|
||
useEffect(() => { | ||
resetLines(); | ||
render(); | ||
}, [category]); | ||
|
||
const getChildGroup = ( | ||
p: PickerConfigType, | ||
lines: { name: string; id: string; hidden: boolean }[] | ||
) => { | ||
const res = lines | ||
.filter((line) => containsAllSubstrings(line.id, p.tags)) | ||
.map((line) => { | ||
return { | ||
id: line.id, | ||
name: line.name, | ||
checked: !line.hidden, | ||
}; | ||
}); | ||
return res; | ||
}; | ||
|
||
const changeLineCateory = (category: string) => { | ||
setCategory(category); | ||
}; | ||
|
||
const changeLineVisilibity = (checked: CheckboxItem[]) => { | ||
const newLines = lines.map((line) => { | ||
const checkedItem = checked.find((item) => item.id === line.id); | ||
if (checkedItem) { | ||
line.hidden = !checkedItem.checked; | ||
} | ||
return line; | ||
}); | ||
setLines(newLines); | ||
}; | ||
|
||
return ( | ||
<div> | ||
{options.length > 0 && ( | ||
<div className={styles.rowFlexCenter}> | ||
<div>Group by:</div> | ||
<DropList onChange={changeLineCateory} options={options}></DropList> | ||
</div> | ||
)} | ||
{groups.length > 0 && ( | ||
<div className={styles.linePickerGroup}> | ||
<ChartCheckboxGroups | ||
groups={groups} | ||
onChange={changeLineVisilibity} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default RenderLinePickerOptions; |
45 changes: 45 additions & 0 deletions
45
torchci/components/charts/line_rect_chart/component/RenderSvgLine.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import * as d3 from "d3"; | ||
import { getRandomColor } from "../lib/color"; | ||
import { D3LineRecord, Line } from "../lib/types"; | ||
|
||
/** | ||
* handle svg line rendering for LineRectChart | ||
*/ | ||
const RenderSvgLines = ({ | ||
scales, | ||
lines, | ||
lineConfigs, | ||
}: { | ||
scales: any; | ||
lines: Line[]; | ||
lineConfigs: { name: string; id: string; hidden: boolean }[]; | ||
}) => { | ||
const lineGenerator = d3 | ||
.line<D3LineRecord>() | ||
.x((d: D3LineRecord) => scales.xScale(d.date)) | ||
.y((d: D3LineRecord) => scales.yScale(d.value)) | ||
.curve(d3.curveBasis); | ||
|
||
return ( | ||
<g className="lines-group"> | ||
{lines.map((line, i) => { | ||
const hidden = | ||
lineConfigs.find((config) => config.id === line.id)?.hidden ?? false; | ||
return ( | ||
<path | ||
key={i} | ||
d={lineGenerator(line.records)?.toString()} | ||
id={line.name + "-line"} | ||
className={"line"} | ||
fill="none" | ||
opacity={hidden ? 0.05 : 1} | ||
stroke={line.color ? line.color : getRandomColor(i)} | ||
strokeWidth={1.2} | ||
/> | ||
); | ||
})} | ||
</g> | ||
); | ||
}; | ||
|
||
export default RenderSvgLines; |
Oops, something went wrong.