Skip to content

Commit 8842368

Browse files
authored
Add top-level plots wizard (#4586)
1 parent c4b24d1 commit 8842368

File tree

16 files changed

+786
-19
lines changed

16 files changed

+786
-19
lines changed

extension/package.json

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@
423423
"category": "DVC",
424424
"icon": "$(symbol-class)"
425425
},
426+
{
427+
"title": "Add Plot",
428+
"command": "dvc.addTopLevelPlot",
429+
"category": "DVC",
430+
"icon": "$(add)"
431+
},
426432
{
427433
"title": "Show Plots",
428434
"command": "dvc.showPlots",
@@ -903,6 +909,10 @@
903909
"command": "dvc.showPipelineDAG",
904910
"when": "dvc.commands.available && dvc.project.available"
905911
},
912+
{
913+
"command": "dvc.addTopLevelPlot",
914+
"when": "dvc.commands.available && dvc.project.available"
915+
},
906916
{
907917
"command": "dvc.showExperiments",
908918
"when": "dvc.commands.available && dvc.project.available"
@@ -1414,6 +1424,11 @@
14141424
"when": "view == dvc.views.experimentsFilterByTree && dvc.experiments.filtered",
14151425
"group": "navigation@3"
14161426
},
1427+
{
1428+
"command": "dvc.addTopLevelPlot",
1429+
"when": "view == dvc.views.plotsPathsTree",
1430+
"group": "navigation@0"
1431+
},
14171432
{
14181433
"command": "dvc.showPlots",
14191434
"when": "view == dvc.views.plotsPathsTree",
@@ -1704,7 +1719,6 @@
17041719
"appdirs": "1.1.0",
17051720
"execa": "5.1.1",
17061721
"fs-extra": "11.1.1",
1707-
"js-yaml": "4.1.0",
17081722
"json-2-csv": "4.1.0",
17091723
"json5": "2.2.3",
17101724
"lodash.clonedeep": "4.5.0",
@@ -1718,7 +1732,8 @@
17181732
"tree-kill": "1.2.2",
17191733
"uuid": "9.0.0",
17201734
"vega-util": "1.17.2",
1721-
"vscode-languageclient": "8.1.0"
1735+
"vscode-languageclient": "8.1.0",
1736+
"yaml": "2.3.2"
17221737
},
17231738
"devDependencies": {
17241739
"@swc/core": "1.3.82",
@@ -1728,7 +1743,6 @@
17281743
"@types/copy-webpack-plugin": "10.1.0",
17291744
"@types/fs-extra": "11.0.1",
17301745
"@types/jest": "29.5.4",
1731-
"@types/js-yaml": "4.0.5",
17321746
"@types/lodash.clonedeep": "4.5.7",
17331747
"@types/lodash.get": "4.4.7",
17341748
"@types/lodash.isempty": "4.4.7",

extension/src/cli/dvc/contract.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { Plot } from '../../plots/webview/contract'
33
export const MIN_CLI_VERSION = '2.58.1'
44
export const LATEST_TESTED_CLI_VERSION = '3.19.0'
55

6+
export const PLOT_TEMPLATES = [
7+
'simple',
8+
'linear',
9+
'confusion',
10+
'confusion_normalized',
11+
'scatter',
12+
'scatter_jitter',
13+
'smooth',
14+
'bar_horizontal_sorted',
15+
'bar_horizontal'
16+
]
17+
618
type ErrorContents = { type: string; msg: string }
719

820
export type DvcError = { error: ErrorContents }

extension/src/commands/external.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export enum RegisteredCommands {
7272
STOP_EXPERIMENTS = 'dvc.stopAllRunningExperiments',
7373

7474
PIPELINE_SHOW_DAG = 'dvc.showPipelineDAG',
75+
PIPELINE_ADD_PLOT = 'dvc.addTopLevelPlot',
7576

7677
PLOTS_PATH_TOGGLE = 'dvc.views.plotsPathsTree.toggleStatus',
7778
PLOTS_SHOW = 'dvc.showPlots',

extension/src/fileSystem/index.test.ts

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
writeTsv,
2222
isPathInProject,
2323
getPidFromFile,
24-
getEntryFromJsonFile
24+
getEntryFromJsonFile,
25+
addPlotToDvcYamlFile,
26+
loadDataFile
2527
} from '.'
2628
import { dvcDemoPath } from '../test/util'
2729
import { DOT_DVC } from '../cli/dvc/constants'
@@ -61,6 +63,83 @@ beforeEach(() => {
6163
jest.resetAllMocks()
6264
})
6365

66+
describe('loadDataFile', () => {
67+
it('should load in csv file contents', async () => {
68+
const mockCsvContent = ['epoch,acc', '10,0.69', '11,0.345'].join('\n')
69+
70+
mockedReadFileSync.mockReturnValueOnce(mockCsvContent)
71+
72+
const result = await loadDataFile('values.csv')
73+
74+
expect(result).toStrictEqual([
75+
{ acc: 0.69, epoch: 10 },
76+
{ acc: 0.345, epoch: 11 }
77+
])
78+
})
79+
80+
it('should load in json file contents', async () => {
81+
const mockJsonContent = JSON.stringify([
82+
{ acc: 0.69, epoch: 10 },
83+
{ acc: 0.345, epoch: 11 }
84+
])
85+
86+
mockedReadFileSync.mockReturnValueOnce(mockJsonContent)
87+
88+
const result = await loadDataFile('values.json')
89+
90+
expect(result).toStrictEqual([
91+
{ acc: 0.69, epoch: 10 },
92+
{ acc: 0.345, epoch: 11 }
93+
])
94+
})
95+
96+
it('should load in tsv file contents', async () => {
97+
const mockTsvContent = ['epoch\tacc', '10\t0.69', '11\t0.345'].join('\n')
98+
99+
mockedReadFileSync.mockReturnValueOnce(mockTsvContent)
100+
101+
const result = await loadDataFile('values.tsv')
102+
103+
expect(result).toStrictEqual([
104+
{ acc: 0.69, epoch: 10 },
105+
{ acc: 0.345, epoch: 11 }
106+
])
107+
})
108+
109+
it('should load in yaml file contents', async () => {
110+
const mockYamlContent = [
111+
'stages:',
112+
' train:',
113+
' cmd: python train.py'
114+
].join('\n')
115+
116+
mockedReadFileSync.mockReturnValueOnce(mockYamlContent)
117+
118+
const result = await loadDataFile('dvc.yaml')
119+
120+
expect(result).toStrictEqual({
121+
stages: {
122+
train: {
123+
cmd: 'python train.py'
124+
}
125+
}
126+
})
127+
})
128+
129+
it('should catch any errors thrown during file parsing', async () => {
130+
const dataFiles = ['values.csv', 'file.json', 'file.tsv', 'dvc.yaml']
131+
mockedReadFileSync.mockImplementation(() => {
132+
throw new Error('fake error')
133+
})
134+
135+
for (const file of dataFiles) {
136+
const resultWithErr = await loadDataFile(file)
137+
138+
expect(resultWithErr).toStrictEqual(undefined)
139+
}
140+
})
141+
})
142+
64143
describe('writeJson', () => {
65144
it('should write unformatted json in given file', () => {
66145
writeJson('file-name.json', { array: [1, 2, 3], number: 1 })
@@ -436,6 +515,124 @@ describe('findOrCreateDvcYamlFile', () => {
436515
})
437516
})
438517

518+
describe('addPlotToDvcYamlFile', () => {
519+
const mockStagesLines = ['stages:', ' train:', ' cmd: python train.py']
520+
const mockPlotsListLines = [
521+
'plots:',
522+
' - eval/importance.png',
523+
' - Precision-Recall:',
524+
' x: recall',
525+
' y:',
526+
' eval/prc/train.json: precision',
527+
' eval/prc/test.json: precision'
528+
]
529+
const mockNewPlotLines = [
530+
' - data.json:',
531+
' template: simple',
532+
' x: epochs',
533+
' y: accuracy'
534+
]
535+
it('should add a plots list with the new plot if the dvc.yaml file has no plots', () => {
536+
const mockDvcYamlContent = mockStagesLines.join('\n')
537+
const mockPlotYamlContent = ['', 'plots:', ...mockNewPlotLines, ''].join(
538+
'\n'
539+
)
540+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent)
541+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent)
542+
543+
addPlotToDvcYamlFile('/', {
544+
dataFile: '/data.json',
545+
template: 'simple',
546+
x: 'epochs',
547+
y: 'accuracy'
548+
})
549+
550+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
551+
'//dvc.yaml',
552+
mockDvcYamlContent + mockPlotYamlContent
553+
)
554+
})
555+
556+
it('should add the new plot if the dvc.yaml file already has plots', () => {
557+
const mockDvcYamlContent = [...mockPlotsListLines, ...mockStagesLines]
558+
const mockPlotYamlContent = [...mockNewPlotLines, '']
559+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent.join('\n'))
560+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent.join('\n'))
561+
562+
addPlotToDvcYamlFile('/', {
563+
dataFile: '/data.json',
564+
template: 'simple',
565+
x: 'epochs',
566+
y: 'accuracy'
567+
})
568+
569+
mockDvcYamlContent.splice(7, 0, ...mockPlotYamlContent)
570+
571+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
572+
'//dvc.yaml',
573+
mockDvcYamlContent.join('\n')
574+
)
575+
})
576+
577+
it('should add a new plot if the dvc.yaml plots list is at bottom of file', () => {
578+
const mockDvcYamlContent = [...mockStagesLines, ...mockPlotsListLines].join(
579+
'\n'
580+
)
581+
const mockPlotYamlContent = ['', ...mockNewPlotLines, ''].join('\n')
582+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent)
583+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent)
584+
585+
addPlotToDvcYamlFile('/', {
586+
dataFile: '/data.json',
587+
template: 'simple',
588+
x: 'epochs',
589+
y: 'accuracy'
590+
})
591+
592+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
593+
'//dvc.yaml',
594+
mockDvcYamlContent + mockPlotYamlContent
595+
)
596+
})
597+
598+
it('should add a new plot with an indent level that matches the dvc.yaml file', () => {
599+
const mockDvcYamlContent = [
600+
'stages:',
601+
' train:',
602+
' cmd: python train.py',
603+
'plots:',
604+
' - eval/importance.png',
605+
' - Precision-Recall:',
606+
' x: recall',
607+
' y:',
608+
' eval/prc/train.json: precision',
609+
' eval/prc/test.json: precision'
610+
].join('\n')
611+
const mockPlotYamlContent = [
612+
'',
613+
' - data.json:',
614+
' template: simple',
615+
' x: epochs',
616+
' y: accuracy',
617+
''
618+
].join('\n')
619+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent)
620+
mockedReadFileSync.mockReturnValueOnce(mockDvcYamlContent)
621+
622+
addPlotToDvcYamlFile('/', {
623+
dataFile: '/data.json',
624+
template: 'simple',
625+
x: 'epochs',
626+
y: 'accuracy'
627+
})
628+
629+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
630+
'//dvc.yaml',
631+
mockDvcYamlContent + mockPlotYamlContent
632+
)
633+
})
634+
})
635+
439636
describe('isPathInProject', () => {
440637
it('should return true if the path is in the project', () => {
441638
const path = join(dvcDemoPath, 'dvc.yaml')

0 commit comments

Comments
 (0)