Skip to content

Commit 6d64a36

Browse files
authored
Add "Export Raw Data" option to plot popup (#4181)
1 parent 51991e6 commit 6d64a36

File tree

15 files changed

+401
-21
lines changed

15 files changed

+401
-21
lines changed

extension/src/fileSystem/index.test.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { join, relative, resolve } from 'path'
2-
import { appendFileSync, ensureDirSync, ensureFileSync, remove } from 'fs-extra'
2+
import {
3+
appendFileSync,
4+
ensureDirSync,
5+
ensureFileSync,
6+
remove,
7+
writeFileSync
8+
} from 'fs-extra'
39
import { TextDocument, window, workspace } from 'vscode'
410
import {
511
exists,
@@ -8,7 +14,8 @@ import {
814
isDirectory,
915
isSameOrChild,
1016
getModifiedTime,
11-
findOrCreateDvcYamlFile
17+
findOrCreateDvcYamlFile,
18+
writeJson
1219
} from '.'
1320
import { dvcDemoPath } from '../test/util'
1421
import { DOT_DVC } from '../cli/dvc/constants'
@@ -21,12 +28,14 @@ jest.mock('fs-extra', () => {
2128
__esModule: true,
2229
...actualModule,
2330
appendFileSync: jest.fn(),
24-
ensureFileSync: jest.fn()
31+
ensureFileSync: jest.fn(),
32+
writeFileSync: jest.fn()
2533
}
2634
})
2735

2836
const mockedAppendFileSync = jest.mocked(appendFileSync)
2937
const mockedEnsureFileSync = jest.mocked(ensureFileSync)
38+
const mockedWriteFileSync = jest.mocked(writeFileSync)
3039
const mockedWorkspace = jest.mocked(workspace)
3140
const mockedWindow = jest.mocked(window)
3241
const mockedOpenTextDocument = jest.fn()
@@ -39,6 +48,29 @@ beforeEach(() => {
3948
jest.resetAllMocks()
4049
})
4150

51+
describe('writeJson', () => {
52+
it('should write unformatted json in given file', () => {
53+
writeJson('file-name.json', { array: [1, 2, 3], number: 1 })
54+
55+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
56+
'file-name.json',
57+
'{"array":[1,2,3],"number":1}'
58+
)
59+
})
60+
61+
it('should write formatted json in given file', () => {
62+
writeJson('file-name.json', { array: [1, 2, 3], number: 1 }, true)
63+
64+
const formattedJson =
65+
'{\n "array": [\n 1,\n 2,\n 3\n ],\n "number": 1\n}'
66+
67+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
68+
'file-name.json',
69+
formattedJson
70+
)
71+
})
72+
})
73+
4274
describe('findDvcRootPaths', () => {
4375
it('should find the dvc root if it exists in the given folder', async () => {
4476
const dvcRoots = await findDvcRootPaths(dvcDemoPath)

extension/src/fileSystem/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,12 @@ export const loadJson = <T>(path: string): T | undefined => {
210210

211211
export const writeJson = <T extends Record<string, unknown>>(
212212
path: string,
213-
obj: T
213+
obj: T,
214+
format = false
214215
): void => {
215216
ensureFileSync(path)
216-
return writeFileSync(path, JSON.stringify(obj))
217+
const json = format ? JSON.stringify(obj, null, 4) : JSON.stringify(obj)
218+
return writeFileSync(path, json)
217219
}
218220

219221
export const getPidFromFile = async (
@@ -266,3 +268,8 @@ export const getBinDisplayText = (
266268
? '.' + sep + relative(workspaceRoot, path)
267269
: path
268270
}
271+
272+
export const showSaveDialog = (
273+
defaultUri: Uri,
274+
filters?: { [name: string]: string[] }
275+
) => window.showSaveDialog({ defaultUri, filters })

extension/src/plots/model/collect.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ export const collectCustomPlots = ({
108108
return plots
109109
}
110110

111+
export const collectCustomPlotRawData = (
112+
orderValue: CustomPlotsOrderValue,
113+
experiments: Experiment[]
114+
) => {
115+
const { metric, param } = orderValue
116+
const metricPath = getFullValuePath(ColumnType.METRICS, metric)
117+
const paramPath = getFullValuePath(ColumnType.PARAMS, param)
118+
119+
return getValues(experiments, metricPath, paramPath)
120+
}
121+
111122
type RevisionPathData = { [path: string]: Record<string, unknown>[] }
112123

113124
export type RevisionData = {
@@ -433,9 +444,37 @@ export const collectSelectedTemplatePlots = (
433444
group
434445
})
435446
}
447+
436448
return acc.length > 0 ? acc : undefined
437449
}
438450

451+
export const collectSelectedTemplatePlotRawData = ({
452+
selectedRevisions,
453+
path,
454+
template,
455+
revisionData,
456+
multiSourceEncodingUpdate
457+
}: {
458+
selectedRevisions: string[]
459+
path: string
460+
template: string
461+
revisionData: RevisionData
462+
multiSourceEncodingUpdate: { strokeDash: StrokeDashEncoding }
463+
}) => {
464+
const isMultiView = isMultiViewPlot(
465+
JSON.parse(template) as TopLevelSpec | VisualizationSpec
466+
)
467+
const { datapoints } = transformRevisionData(
468+
path,
469+
selectedRevisions,
470+
revisionData,
471+
isMultiView,
472+
multiSourceEncodingUpdate
473+
)
474+
475+
return datapoints
476+
}
477+
439478
export const collectOrderedRevisions = (
440479
revisions: {
441480
id: string

extension/src/plots/model/index.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
getCustomPlotId,
1111
collectOrderedRevisions,
1212
collectImageUrl,
13-
collectIdShas
13+
collectIdShas,
14+
collectSelectedTemplatePlotRawData,
15+
collectCustomPlotRawData
1416
} from './collect'
1517
import { getRevisionSummaryColumns } from './util'
1618
import {
@@ -55,6 +57,8 @@ import {
5557
} from '../multiSource/collect'
5658
import { isDvcError } from '../../cli/dvc/reader'
5759
import { ErrorsModel } from '../errors/model'
60+
import { openFileInEditor, writeJson } from '../../fileSystem'
61+
import { Toast } from '../../vscode/toast'
5862

5963
export class PlotsModel extends ModelWithPersistence {
6064
private readonly experiments: Experiments
@@ -223,6 +227,23 @@ export class PlotsModel extends ModelWithPersistence {
223227
return selectedRevisions
224228
}
225229

230+
public savePlotData(plotId: string, filePath: string) {
231+
const foundCustomPlot = this.customPlotsOrder.find(
232+
({ metric, param }) => getCustomPlotId(metric, param) === plotId
233+
)
234+
235+
const rawData = foundCustomPlot
236+
? this.getCustomPlotData(foundCustomPlot)
237+
: this.getSelectedTemplatePlotData(plotId)
238+
239+
try {
240+
writeJson(filePath, rawData as unknown as Record<string, unknown>, true)
241+
void openFileInEditor(filePath)
242+
} catch {
243+
void Toast.showError('Cannot write to file')
244+
}
245+
}
246+
226247
public getTemplatePlots(
227248
order: TemplateOrder | undefined,
228249
selectedRevisions: Revision[]
@@ -438,4 +459,24 @@ export class PlotsModel extends ModelWithPersistence {
438459
this.multiSourceEncoding
439460
)
440461
}
462+
463+
private getSelectedTemplatePlotData(path: string) {
464+
const selectedRevisions = this.getSelectedRevisionDetails()
465+
466+
return collectSelectedTemplatePlotRawData({
467+
multiSourceEncodingUpdate: this.multiSourceEncoding[path] || {},
468+
path,
469+
revisionData: this.revisionData,
470+
selectedRevisions: selectedRevisions.map(({ id }) => id),
471+
template: this.templates[path]
472+
})
473+
}
474+
475+
private getCustomPlotData(orderValue: CustomPlotsOrderValue) {
476+
const experiments = this.experiments
477+
.getWorkspaceCommitsAndExperiments()
478+
.filter(({ id }) => id !== EXPERIMENT_WORKSPACE_ID)
479+
480+
return collectCustomPlotRawData(orderValue, experiments)
481+
}
441482
}

extension/src/plots/webview/messages.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { commands } from 'vscode'
1+
import { Uri, commands } from 'vscode'
22
import isEmpty from 'lodash.isempty'
33
import {
44
ComparisonPlot,
@@ -21,7 +21,11 @@ import {
2121
import { PlotsModel } from '../model'
2222
import { PathsModel } from '../paths/model'
2323
import { BaseWebview } from '../../webview'
24-
import { getModifiedTime, openImageFileInEditor } from '../../fileSystem'
24+
import {
25+
getModifiedTime,
26+
openImageFileInEditor,
27+
showSaveDialog
28+
} from '../../fileSystem'
2529
import { reorderObjectList } from '../../util/array'
2630
import { CustomPlotsOrderValue } from '../model/custom'
2731
import { getCustomPlotId } from '../model/collect'
@@ -78,6 +82,8 @@ export class WebviewMessages {
7882
RegisteredCommands.PLOTS_CUSTOM_ADD,
7983
this.dvcRoot
8084
)
85+
case MessageFromWebviewType.EXPORT_PLOT_DATA:
86+
return this.exportPlotData(message.payload)
8187
case MessageFromWebviewType.RESIZE_PLOTS:
8288
return this.setPlotSize(
8389
message.payload.section,
@@ -337,4 +343,20 @@ export class WebviewMessages {
337343
private getCustomPlots() {
338344
return this.plots.getCustomPlots() || null
339345
}
346+
347+
private async exportPlotData(plotId: string) {
348+
const file = await showSaveDialog(Uri.file('data.json'), { JSON: ['json'] })
349+
350+
if (!file) {
351+
return
352+
}
353+
354+
sendTelemetryEvent(
355+
EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA,
356+
undefined,
357+
undefined
358+
)
359+
360+
this.plots.savePlotData(plotId, file.path)
361+
}
340362
}

extension/src/telemetry/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const EventName = Object.assign(
7171
'views.plots.comparisonRowsReordered',
7272
VIEWS_PLOTS_CREATED: 'views.plots.created',
7373
VIEWS_PLOTS_EXPERIMENT_TOGGLE: 'views.plots.toggleExperimentStatus',
74+
VIEWS_PLOTS_EXPORT_PLOT_DATA: 'views.plots.exportPlotData',
7475
VIEWS_PLOTS_FOCUS_CHANGED: 'views.plots.focusChanged',
7576
VIEWS_PLOTS_REVISIONS_REORDERED: 'views.plots.revisionsReordered',
7677
VIEWS_PLOTS_SECTION_RESIZED: 'views.plots.sectionResized',
@@ -264,6 +265,8 @@ export interface IEventNamePropertyMapping {
264265
[EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS]: undefined
265266
[EventName.VIEWS_PLOTS_SELECT_PLOTS]: undefined
266267
[EventName.VIEWS_PLOTS_EXPERIMENT_TOGGLE]: undefined
268+
[EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA]: undefined
269+
267270
[EventName.VIEWS_PLOTS_ZOOM_PLOT]: { isImage: boolean }
268271
[EventName.VIEWS_REORDER_PLOTS_CUSTOM]: undefined
269272
[EventName.VIEWS_REORDER_PLOTS_TEMPLATES]: undefined

0 commit comments

Comments
 (0)