Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Custom Plots Section #3342

Merged
merged 29 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0d98524
wip on custom plots section
julieg18 Feb 23, 2023
2da1c1e
delete plot fix
julieg18 Feb 23, 2023
165113c
some refactoring
julieg18 Feb 23, 2023
175e89e
Clear rest of personal comments
julieg18 Feb 24, 2023
f571dd9
Fix tests
julieg18 Feb 24, 2023
66bd1b1
Merge branch 'main' into add-metric-param-plots
julieg18 Feb 24, 2023
ac8e910
Add "backend" tests
julieg18 Feb 27, 2023
2a791c4
Add rest of backend tests
julieg18 Feb 27, 2023
1ed3e8f
Add storybook
julieg18 Feb 27, 2023
e276074
Add front end tests
julieg18 Feb 27, 2023
67096ab
Merge branch 'main' into add-metric-param-plots
julieg18 Feb 27, 2023
f6b4876
Fix broken CustomPlot
julieg18 Feb 27, 2023
154b736
Reduce duplicate code
julieg18 Feb 27, 2023
5e75dfb
Add tests for quick pick
julieg18 Feb 27, 2023
83392e5
Remove unneeded types
julieg18 Feb 28, 2023
a216927
Fix typo
julieg18 Feb 28, 2023
c7b3afe
Remove loading logic in CustomPlots
julieg18 Feb 28, 2023
981f613
Merge branch 'main' into add-metric-param-plots
julieg18 Feb 28, 2023
93a996b
Improve testing
julieg18 Feb 28, 2023
aacb527
Move Toast code out of model
julieg18 Feb 28, 2023
86f9f07
Don't pin 0 on y axis
julieg18 Feb 28, 2023
6036be3
Fix App.test.js error
julieg18 Feb 28, 2023
d664b87
Make custom plots tooltip more clear
julieg18 Feb 28, 2023
9b4c889
Add missing custom plots reordering logic
julieg18 Feb 28, 2023
3c2739a
Merge branch 'main' into add-metric-param-plots
julieg18 Feb 28, 2023
b96068e
Merge branch 'main' into add-metric-param-plots
julieg18 Mar 1, 2023
aa9e592
Have plots update on experiment deletion
julieg18 Mar 1, 2023
fbf6e2c
Undo menu rename
julieg18 Mar 1, 2023
74e70a0
Merge branch 'main' into add-metric-param-plots
julieg18 Mar 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions extension/src/experiments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ export class Experiments extends BaseRepository<TableData> {
return this.experiments.getFinishedExperiments()
}

public getExperiments(): Experiment[] {
return this.experiments.getExperiments()
}

public getExperimentDisplayName(experimentId: string) {
const experiment = this.experiments
.getCombinedList()
Expand Down Expand Up @@ -500,6 +504,10 @@ export class Experiments extends BaseRepository<TableData> {
return this.columns.getFirstThreeColumnOrder()
}

public getColumnTerminalNodes() {
return this.columns.getTerminalNodes()
}

public getHasData() {
if (this.deferred.state === 'none') {
return
Expand Down
2 changes: 1 addition & 1 deletion extension/src/experiments/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ export class ExperimentsModel extends ModelWithPersistence {
return collectFlatExperimentParams(params)
}

public getExperiments() {
public getExperiments(): Experiment[] {
return this.getExperimentsAndQueued().filter(({ status }) => {
return !isQueued(status)
})
Expand Down
1 change: 1 addition & 0 deletions extension/src/persistence/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum PersistenceKey {
PLOT_COMPARISON_ORDER = 'plotComparisonOrder:',
PLOT_COMPARISON_PATHS_ORDER = 'plotComparisonPathsOrder',
PLOT_METRIC_ORDER = 'plotMetricOrder:',
PLOTS_CUSTOM_ORDER = 'plotCustomOrder:',
PLOT_SECTION_COLLAPSED = 'plotSectionCollapsed:',
PLOT_SELECTED_METRICS = 'plotSelectedMetrics:',
PLOT_SIZES = 'plotSizes:',
Expand Down
60 changes: 59 additions & 1 deletion extension/src/plots/model/collect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
collectCheckpointPlotsData,
collectTemplates,
collectMetricOrder,
collectOverrideRevisionDetails
collectOverrideRevisionDetails,
collectCustomPlotsData
} from './collect'
import plotsDiffFixture from '../../test/fixtures/plotsDiff/output'
import expShowFixture from '../../test/fixtures/expShow/base/output'
import modifiedFixture from '../../test/fixtures/expShow/modified/output'
import checkpointPlotsFixture from '../../test/fixtures/expShow/base/checkpointPlots'
import customPlotsFixture from '../../test/fixtures/expShow/base/customPlots'
import {
ExperimentsOutput,
ExperimentStatus,
Expand All @@ -27,6 +29,62 @@ const logsLossPath = join('logs', 'loss.tsv')

const logsLossPlot = (plotsDiffFixture[logsLossPath][0] || {}) as TemplatePlot

describe('collectCustomPlotsData', () => {
it('should return the expected data from the text fixture', () => {
const data = collectCustomPlotsData(
[
{
metric: 'metrics:summary.json:loss',
param: 'params:params.yaml:dropout'
},
{
metric: 'metrics:summary.json:accuracy',
param: 'params:params.yaml:epochs'
}
],
[
{
id: '12345',
label: '123',
metrics: {
'summary.json': {
accuracy: 0.4668000042438507,
loss: 2.0205044746398926
}
},
name: 'exp-e7a67',
params: { 'params.yaml': { dropout: 0.15, epochs: 16 } }
},
{
id: '12345',
label: '123',
metrics: {
'summary.json': {
accuracy: 0.3484833240509033,
loss: 1.9293040037155151
}
},
name: 'exp-83425',
params: { 'params.yaml': { dropout: 0.25, epochs: 10 } }
},
{
id: '12345',
label: '123',
metrics: {
'summary.json': {
accuracy: 0.6768440509033,
loss: 2.298503875732422
}
},
name: 'exp-f13bca',
params: { 'params.yaml': { dropout: 0.32, epochs: 20 } }
}
]
)
expect(data).toStrictEqual(customPlotsFixture.plots)
})
})

describe('collectCheckpointPlotsData', () => {
it('should return the expected data from the test fixture', () => {
const data = collectCheckpointPlotsData(expShowFixture)
Expand Down
50 changes: 48 additions & 2 deletions extension/src/plots/model/collect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import omit from 'lodash.omit'
import get from 'lodash.get'
import { TopLevelSpec } from 'vega-lite'
import { VisualizationSpec } from 'react-vega'
import { getRevisionFirstThreeColumns } from './util'
Expand All @@ -13,7 +14,8 @@ import {
TemplatePlotEntry,
TemplatePlotSection,
PlotsType,
Revision
Revision,
CustomPlotData
} from '../webview/contract'
import {
EXPERIMENT_WORKSPACE_ID,
Expand All @@ -28,9 +30,11 @@ import {
import { extractColumns } from '../../experiments/columns/extract'
import {
decodeColumn,
appendColumnToPath
appendColumnToPath,
splitColumnPath
} from '../../experiments/columns/paths'
import {
ColumnType,
Experiment,
isRunning,
MetricOrParamColumns
Expand Down Expand Up @@ -243,6 +247,48 @@ export const collectCheckpointPlotsData = (
return plotsData
}

export const getCustomPlotId = (metric: string, param: string) =>
`custom-${metric}-${param}`

const collectCustomPlotData = (
metric: string,
param: string,
experiments: Experiment[]
): CustomPlotData => {
const splitUpMetricPath = splitColumnPath(metric)
const splitUpParamPath = splitColumnPath(param)
const plotData: CustomPlotData = {
id: getCustomPlotId(metric, param),
metric: metric.slice(ColumnType.METRICS.length + 1),
param: param.slice(ColumnType.PARAMS.length + 1),
values: []
}

for (const experiment of experiments) {
const metricValue = get(experiment, splitUpMetricPath) as number | undefined
const paramValue = get(experiment, splitUpParamPath) as number | undefined

if (metricValue !== undefined && paramValue !== undefined) {
plotData.values.push({
expName: experiment.name || experiment.label,
metric: metricValue,
param: paramValue
})
}
}

return plotData
}

export const collectCustomPlotsData = (
metricsAndParams: { metric: string; param: string }[],
experiments: Experiment[]
): CustomPlotData[] => {
return metricsAndParams.map(({ metric, param }) =>
collectCustomPlotData(metric, param, experiments)
)
}

type MetricOrderAccumulator = {
newOrder: string[]
uncollectedMetrics: string[]
Expand Down
1 change: 1 addition & 0 deletions extension/src/plots/model/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ describe('plotsModel', () => {
const expectedSectionCollapsed = {
[Section.CHECKPOINT_PLOTS]: true,
[Section.TEMPLATE_PLOTS]: false,
[Section.CUSTOM_PLOTS]: false,
[Section.COMPARISON_TABLE]: false
}

Expand Down
67 changes: 64 additions & 3 deletions extension/src/plots/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
RevisionData,
TemplateAccumulator,
collectCommitRevisionDetails,
collectOverrideRevisionDetails
collectOverrideRevisionDetails,
collectCustomPlotsData,
getCustomPlotId
} from './collect'
import { getRevisionFirstThreeColumns } from './util'
import {
Expand All @@ -24,7 +26,8 @@ import {
DEFAULT_SECTION_SIZES,
Section,
SectionCollapsed,
PlotSizeNumber
PlotSizeNumber,
CustomPlotData
} from '../webview/contract'
import {
ExperimentsOutput,
Expand All @@ -45,11 +48,13 @@ import {
MultiSourceVariations
} from '../multiSource/collect'
import { isDvcError } from '../../cli/dvc/reader'
import { Toast } from '../../vscode/toast'

export class PlotsModel extends ModelWithPersistence {
private readonly experiments: Experiments

private plotSizes: Record<Section, number>
private customPlotsOrder: { metric: string; param: string }[]
private sectionCollapsed: SectionCollapsed
private commitRevisions: Record<string, string> = {}

Expand All @@ -64,6 +69,7 @@ export class PlotsModel extends ModelWithPersistence {
private multiSourceEncoding: MultiSourceEncoding = {}

private checkpointPlots?: CheckpointPlot[]
private customPlots?: CustomPlotData[]
private selectedMetrics?: string[]
private metricOrder: string[]

Expand All @@ -89,6 +95,8 @@ export class PlotsModel extends ModelWithPersistence {
undefined
)
this.metricOrder = this.revive(PersistenceKey.PLOT_METRIC_ORDER, [])

this.customPlotsOrder = this.revive(PersistenceKey.PLOTS_CUSTOM_ORDER, [])
}

public transformAndSetExperiments(data: ExperimentsOutput) {
Expand Down Expand Up @@ -119,6 +127,8 @@ export class PlotsModel extends ModelWithPersistence {
collectMultiSourceVariations(data, this.multiSourceVariations)
])

this.recreateCustomPlots()

this.comparisonData = {
...this.comparisonData,
...comparisonData
Expand All @@ -127,7 +137,6 @@ export class PlotsModel extends ModelWithPersistence {
...this.revisionData,
...revisionData
}

this.templates = { ...this.templates, ...templates }
this.multiSourceVariations = multiSourceVariations
this.multiSourceEncoding = collectMultiSourceEncoding(
Expand Down Expand Up @@ -171,6 +180,58 @@ export class PlotsModel extends ModelWithPersistence {
}
}

public getCustomPlots() {
if (!this.customPlots) {
return
}
return {
plots: this.customPlots,
size: this.getPlotSize(Section.CUSTOM_PLOTS)
}
}

public recreateCustomPlots() {
const customPlots: CustomPlotData[] = collectCustomPlotsData(
this.getCustomPlotsOrder(),
this.experiments.getExperiments()
)
this.customPlots = customPlots
julieg18 marked this conversation as resolved.
Show resolved Hide resolved
}

public getCustomPlotsOrder() {
return this.customPlotsOrder
}

public setCustomPlotsOrder(plotsOrder: { metric: string; param: string }[]) {
this.customPlotsOrder = plotsOrder
this.persist(PersistenceKey.PLOTS_CUSTOM_ORDER, this.customPlotsOrder)
this.recreateCustomPlots()
}

public removeCustomPlots(plotIds: string[]) {
const newCustomPlotsOrder = this.getCustomPlotsOrder().filter(
({ metric, param }) => {
return !plotIds.includes(getCustomPlotId(metric, param))
}
)

this.setCustomPlotsOrder(newCustomPlotsOrder)
}

public addCustomPlot(metricAndParam: { metric: string; param: string }) {
const plotAlreadyExists = this.getCustomPlotsOrder().some(
({ param, metric }) =>
param === metricAndParam.param && metric === metricAndParam.metric
)

if (plotAlreadyExists) {
return Toast.showError('Custom plot already exists.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[I] Ideally this class is responsible for data/data manipulation. Plots should use Toast and inform the user when there is a problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could remove the choice from the list instead of letting them select something that already exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could remove the choice from the list instead of letting them select something that already exists.

Definitely doable by looping over the custom plots and checking for what exists already!

}

const newCustomPlotsOrder = [...this.getCustomPlotsOrder(), metricAndParam]
this.setCustomPlotsOrder(newCustomPlotsOrder)
}

public setupManualRefresh(id: string) {
this.deleteRevisionData(id)
}
Expand Down
65 changes: 65 additions & 0 deletions extension/src/plots/model/quickPick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { getCustomPlotId } from './collect'
import { splitColumnPath } from '../../experiments/columns/paths'
import { pickFromColumnLikes } from '../../experiments/columns/quickPick'
import { Column, ColumnType } from '../../experiments/webview/contract'
import { definedAndNonEmpty } from '../../util/array'
import {
quickPickManyValues,
QuickPickOptionsWithTitle
} from '../../vscode/quickPick'
import { Title } from '../../vscode/title'
import { Toast } from '../../vscode/toast'

export const pickCustomPlots = (
plots: { metric: string; param: string }[],
quickPickOptions: QuickPickOptionsWithTitle
): Thenable<string[] | undefined> => {
if (!definedAndNonEmpty(plots)) {
return Toast.showError('There are no plots to remove.')
}

const plotsItems = plots.map(({ metric, param }) => {
const splitMetric = splitColumnPath(metric)
const splitParam = splitColumnPath(param)
return {
description: `${metric} vs ${param}`,
label: `${splitMetric[splitMetric.length - 1]} vs ${
splitParam[splitParam.length - 1]
}`,
value: getCustomPlotId(metric, param)
}
})

return quickPickManyValues(plotsItems, quickPickOptions)
}

export const pickMetricAndParam = async (columns: Column[]) => {
const metricColumnLikes = columns
julieg18 marked this conversation as resolved.
Show resolved Hide resolved
.filter(({ type }) => type === ColumnType.METRICS)
.map(({ label, path }) => ({ label, path }))
const paramColumnLikes = columns
julieg18 marked this conversation as resolved.
Show resolved Hide resolved
.filter(({ type }) => type === ColumnType.PARAMS)
.map(({ label, path }) => ({ label, path }))
if (
!definedAndNonEmpty(metricColumnLikes) ||
!definedAndNonEmpty(paramColumnLikes)
) {
return Toast.showError('There are no metrics or params to select from.')
}
const metric = await pickFromColumnLikes(metricColumnLikes, {
title: Title.SELECT_METRIC_CUSTOM_PLOT
})

if (!metric) {
return
}

const param = await pickFromColumnLikes(paramColumnLikes, {
title: Title.SELECT_PARAM_CUSTOM_PLOT
})

if (!param) {
return
}
return { metric: metric.path, param: param.path }
}
Loading