Skip to content

Commit ca0414c

Browse files
authored
Add CSV export option to zoomed in plots (#4252)
1 parent 2742dce commit ca0414c

File tree

13 files changed

+245
-120
lines changed

13 files changed

+245
-120
lines changed

extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,7 @@
16691669
"execa": "5.1.1",
16701670
"fs-extra": "11.1.1",
16711671
"js-yaml": "4.1.0",
1672+
"json-2-csv": "4.0.0",
16721673
"json5": "2.2.3",
16731674
"lodash.clonedeep": "4.5.0",
16741675
"lodash.get": "4.4.2",

extension/src/fileSystem/index.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
isSameOrChild,
1616
getModifiedTime,
1717
findOrCreateDvcYamlFile,
18-
writeJson
18+
writeJson,
19+
writeCsv
1920
} from '.'
2021
import { dvcDemoPath } from '../test/util'
2122
import { DOT_DVC } from '../cli/dvc/constants'
@@ -71,6 +72,21 @@ describe('writeJson', () => {
7172
})
7273
})
7374

75+
describe('writeCsv', () => {
76+
it('should write csv into given file', async () => {
77+
await writeCsv('file-name.csv', [
78+
{ nested: { string: 'string1' }, value: 3 },
79+
{ nested: { string: 'string2' }, value: 4 },
80+
{ nested: { string: 'string3' }, value: 6 }
81+
])
82+
83+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
84+
'file-name.csv',
85+
'nested.string,value\nstring1,3\nstring2,4\nstring3,6'
86+
)
87+
})
88+
})
89+
7490
describe('findDvcRootPaths', () => {
7591
it('should find the dvc root if it exists in the given folder', async () => {
7692
const dvcRoots = await findDvcRootPaths(dvcDemoPath)

extension/src/fileSystem/index.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from 'fs-extra'
2121
import { load } from 'js-yaml'
2222
import { Uri, workspace, window, commands, ViewColumn } from 'vscode'
23+
import { json2csv } from 'json-2-csv'
2324
import { standardizePath } from './path'
2425
import { definedAndNonEmpty, sortCollectedArray } from '../util/array'
2526
import { Logger } from '../common/logger'
@@ -208,7 +209,9 @@ export const loadJson = <T>(path: string): T | undefined => {
208209
}
209210
}
210211

211-
export const writeJson = <T extends Record<string, unknown>>(
212+
export const writeJson = <
213+
T extends Record<string, unknown> | Array<Record<string, unknown>>
214+
>(
212215
path: string,
213216
obj: T,
214217
format = false
@@ -218,6 +221,15 @@ export const writeJson = <T extends Record<string, unknown>>(
218221
return writeFileSync(path, json)
219222
}
220223

224+
export const writeCsv = async (
225+
path: string,
226+
arr: Array<Record<string, unknown>>
227+
) => {
228+
ensureFileSync(path)
229+
const csv = await json2csv(arr)
230+
return writeFileSync(path, csv)
231+
}
232+
221233
export const getPidFromFile = async (
222234
path: string
223235
): Promise<number | undefined> => {
@@ -269,7 +281,8 @@ export const getBinDisplayText = (
269281
: path
270282
}
271283

272-
export const showSaveDialog = (
273-
defaultUri: Uri,
274-
filters?: { [name: string]: string[] }
275-
) => window.showSaveDialog({ defaultUri, filters })
284+
export const showSaveDialog = (fileName: string, extname: string) =>
285+
window.showSaveDialog({
286+
defaultUri: Uri.file(fileName),
287+
filters: { [extname.toUpperCase()]: [extname] }
288+
})

extension/src/plots/model/collect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export const collectCustomPlots = ({
111111
export const collectCustomPlotRawData = (
112112
orderValue: CustomPlotsOrderValue,
113113
experiments: Experiment[]
114-
) => {
114+
): Array<Record<string, unknown>> => {
115115
const { metric, param } = orderValue
116116
const metricPath = getFullValuePath(ColumnType.METRICS, metric)
117117
const paramPath = getFullValuePath(ColumnType.PARAMS, param)
@@ -472,7 +472,7 @@ export const collectSelectedTemplatePlotRawData = ({
472472
multiSourceEncodingUpdate
473473
)
474474

475-
return datapoints
475+
return datapoints as unknown as Array<Record<string, unknown>>
476476
}
477477

478478
export const collectOrderedRevisions = (

extension/src/plots/model/index.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import {
5757
} from '../multiSource/collect'
5858
import { isDvcError } from '../../cli/dvc/reader'
5959
import { ErrorsModel } from '../errors/model'
60-
import { openFileInEditor, writeJson } from '../../fileSystem'
60+
import { openFileInEditor, writeCsv, writeJson } from '../../fileSystem'
6161
import { Toast } from '../../vscode/toast'
6262

6363
export class PlotsModel extends ModelWithPersistence {
@@ -227,21 +227,15 @@ export class PlotsModel extends ModelWithPersistence {
227227
return selectedRevisions
228228
}
229229

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)
230+
public savePlotDataAsJson(filePath: string, plotId: string) {
231+
void this.savePlotData(filePath, plotId, data => {
232+
writeJson(filePath, data, true)
233+
return Promise.resolve()
234+
})
235+
}
238236

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-
}
237+
public savePlotDataAsCsv(filePath: string, plotId: string) {
238+
void this.savePlotData(filePath, plotId, data => writeCsv(filePath, data))
245239
}
246240

247241
public getTemplatePlots(
@@ -479,4 +473,25 @@ export class PlotsModel extends ModelWithPersistence {
479473

480474
return collectCustomPlotRawData(orderValue, experiments)
481475
}
476+
477+
private async savePlotData(
478+
filePath: string,
479+
plotId: string,
480+
writeToFile: (rawData: Array<Record<string, unknown>>) => Promise<void>
481+
) {
482+
const foundCustomPlot = this.customPlotsOrder.find(
483+
({ metric, param }) => getCustomPlotId(metric, param) === plotId
484+
)
485+
486+
const rawData = foundCustomPlot
487+
? this.getCustomPlotData(foundCustomPlot)
488+
: this.getSelectedTemplatePlotData(plotId)
489+
490+
try {
491+
await writeToFile(rawData)
492+
void openFileInEditor(filePath)
493+
} catch {
494+
void Toast.showError('Cannot write to file')
495+
}
496+
}
482497
}

extension/src/plots/webview/messages.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Uri, commands } from 'vscode'
1+
import { commands } from 'vscode'
22
import isEmpty from 'lodash.isempty'
33
import {
44
ComparisonPlot,
@@ -82,8 +82,10 @@ export class WebviewMessages {
8282
RegisteredCommands.PLOTS_CUSTOM_ADD,
8383
this.dvcRoot
8484
)
85-
case MessageFromWebviewType.EXPORT_PLOT_DATA:
86-
return this.exportPlotData(message.payload)
85+
case MessageFromWebviewType.EXPORT_PLOT_DATA_AS_CSV:
86+
return this.exportPlotDataAsCsv(message.payload)
87+
case MessageFromWebviewType.EXPORT_PLOT_DATA_AS_JSON:
88+
return this.exportPlotDataAsJson(message.payload)
8789
case MessageFromWebviewType.RESIZE_PLOTS:
8890
return this.setPlotSize(
8991
message.payload.section,
@@ -344,19 +346,35 @@ export class WebviewMessages {
344346
return this.plots.getCustomPlots() || null
345347
}
346348

347-
private async exportPlotData(plotId: string) {
348-
const file = await showSaveDialog(Uri.file('data.json'), { JSON: ['json'] })
349+
private async exportPlotDataAsJson(plotId: string) {
350+
const file = await showSaveDialog('data.json', 'json')
349351

350352
if (!file) {
351353
return
352354
}
353355

354356
sendTelemetryEvent(
355-
EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA,
357+
EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON,
356358
undefined,
357359
undefined
358360
)
359361

360-
this.plots.savePlotData(plotId, file.path)
362+
void this.plots.savePlotDataAsJson(file.path, plotId)
363+
}
364+
365+
private async exportPlotDataAsCsv(plotId: string) {
366+
const file = await showSaveDialog('data.csv', 'csv')
367+
368+
if (!file) {
369+
return
370+
}
371+
372+
sendTelemetryEvent(
373+
EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV,
374+
undefined,
375+
undefined
376+
)
377+
378+
void this.plots.savePlotDataAsCsv(file.path, plotId)
361379
}
362380
}

extension/src/telemetry/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ 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',
74+
VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV: 'views.plots.exportPlotDataAsCsv',
75+
VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON: 'views.plots.exportPlotDataAsJson',
7576
VIEWS_PLOTS_FOCUS_CHANGED: 'views.plots.focusChanged',
7677
VIEWS_PLOTS_REVISIONS_REORDERED: 'views.plots.revisionsReordered',
7778
VIEWS_PLOTS_SECTION_RESIZED: 'views.plots.sectionResized',
@@ -267,7 +268,8 @@ export interface IEventNamePropertyMapping {
267268
[EventName.VIEWS_PLOTS_SELECT_EXPERIMENTS]: undefined
268269
[EventName.VIEWS_PLOTS_SELECT_PLOTS]: undefined
269270
[EventName.VIEWS_PLOTS_EXPERIMENT_TOGGLE]: undefined
270-
[EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA]: undefined
271+
[EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_CSV]: undefined
272+
[EventName.VIEWS_PLOTS_EXPORT_PLOT_DATA_AS_JSON]: undefined
271273

272274
[EventName.VIEWS_PLOTS_ZOOM_PLOT]: { isImage: boolean }
273275
[EventName.VIEWS_REORDER_PLOTS_CUSTOM]: undefined

0 commit comments

Comments
 (0)