import * as plotly from "plotly.js-dist" import React, { FC, useEffect, useState } from "react" import { Grid, FormControl, FormLabel, MenuItem, Select, Typography, SelectChangeEvent, useTheme, Box, } from "@mui/material" import { plotlyDarkTemplate } from "./PlotlyDarkMode" const plotDomId = "graph-parallel-coordinate" export const GraphParallelCoordinate: FC<{ study: StudyDetail | null }> = ({ study = null }) => { const theme = useTheme() const [objectiveId, setObjectiveId] = useState(0) const handleObjectiveChange = (event: SelectChangeEvent) => { setObjectiveId(event.target.value as number) } useEffect(() => { if (study !== null) { plotCoordinate(study, objectiveId, theme.palette.mode) } }, [study, objectiveId, theme.palette.mode]) return ( Parallel Coordinate {study !== null && study.directions.length !== 1 ? ( Objective ID: ) : null} ) } const filterFunc = (trial: Trial, objectiveId: number): boolean => { if (trial.state !== "Complete" && trial.state !== "Pruned") { return false } if (trial.values === undefined) { return false } return ( trial.values.length > objectiveId && trial.values[objectiveId] !== "inf" ) } const plotCoordinate = ( study: StudyDetail, objectiveId: number, mode: string ) => { if (document.getElementById(plotDomId) === null) { return } const layout: Partial = { margin: { l: 70, t: 100, r: 50, b: 0, }, template: mode === "dark" ? plotlyDarkTemplate : {}, } if (study.trials.length === 0) { plotly.react(plotDomId, [], layout) return } const filteredTrials = study.trials.filter((t) => filterFunc(t, objectiveId)) const maxLabelLength = 40 const breakLength = maxLabelLength / 2 const ellipsis = "…" const truncateLabelIfTooLong = (originalLabel: string): string => { return originalLabel.length > maxLabelLength ? originalLabel.substring(0, maxLabelLength - ellipsis.length) + ellipsis : originalLabel } const breakLabelIfTooLong = (originalLabel: string): string => { const truncated = truncateLabelIfTooLong(originalLabel) return truncated .split("") .map((c, i) => { return (i + 1) % breakLength == 0 ? c + "
" : c }) .join("") } // Intersection param names const objectiveValues: number[] = filteredTrials.map( (t) => t.values![objectiveId] as number ) const dimensions = [ { label: "Objective value", values: objectiveValues, range: [Math.min(...objectiveValues), Math.max(...objectiveValues)], }, ] study.intersection_search_space.forEach((s) => { const valueStrings = filteredTrials.map((t) => { const param = t.params.find((p) => p.name === s.name) return param!.value }) const isnum = valueStrings.every((v) => { return !isNaN(parseFloat(v)) }) if (isnum) { const values: number[] = valueStrings.map((v) => parseFloat(v)) dimensions.push({ label: breakLabelIfTooLong(s.name), values: values, range: [Math.min(...values), Math.max(...values)], }) } else { // categorical const vocabSet = new Set(valueStrings) const vocabArr = Array.from(vocabSet) const values: number[] = valueStrings.map((v) => vocabArr.findIndex((vocab) => v === vocab) ) const tickvals: number[] = vocabArr.map((v, i) => i) dimensions.push({ label: breakLabelIfTooLong(s.name), values: values, range: [Math.min(...values), Math.max(...values)], // @ts-ignore tickvals: tickvals, ticktext: vocabArr, }) } }) const plotData: Partial[] = [ { type: "parcoords", // @ts-ignore dimensions: dimensions, labelangle: 30, }, ] plotly.react(plotDomId, plotData, layout) }