Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions src/data/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,13 @@ export const loadBinaryDataWrapper = (title: string, arrayBuffer: ArrayBuffer):
return [];
}
};

/**
* Exports a DictTable to DSV format using d3.dsvFormat
* @param table - The DictTable to export
* @returns DSV string representation of the table
*/
export const exportTableToDsv = (table: DictTable, delimiter: string): string => {
// Use d3.dsvFormat to convert the rows array to DSV
return d3.dsvFormat(delimiter).format(table.rows);
};
213 changes: 213 additions & 0 deletions src/views/ChartifactDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React, { FC, useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Typography,
Box
} from '@mui/material';
import { DictTable } from '../components/ComponentType';
import { exportTableToDsv } from '../data/utils';
import { assembleVegaChart } from '../app/utils';

export interface ChartElements {
tableId: string;
chartId: string;
element: any;
}

export interface ChartifactDialogProps {
open: boolean;
handleCloseDialog: () => void;
tables: DictTable[];
chartElements: ChartElements[];
}

export const ChartifactDialog: FC<ChartifactDialogProps> = function ChartifactDialog({
open,
handleCloseDialog,
tables,
chartElements
}) {
// Generate initial title from table names
const getTablesFromChartElements = () => {
return chartElements
.map(ce => tables.find(t => t.id === ce.tableId))
.filter(table => table) as DictTable[];
};

const [title, setTitle] = useState<string>(() =>
generateTitleFromTables(getTablesFromChartElements())
);

// Update title when chart elements change
useEffect(() => {
if (open) {
const newTitle = generateTitleFromTables(getTablesFromChartElements());
setTitle(newTitle);
}
}, [open, chartElements, tables]);

const handleDownload = () => {

const content = createChartifact(chartElements, tables, title);

// Create a blob and download the file
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// Use title as filename, replace bad chars and spaces with underscores
const sanitizedTitle = title.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_');
link.download = `${sanitizedTitle}.idoc.md`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);

// Close the dialog after download
handleCloseDialog();
setTitle(''); // Reset the title
};

const handleClose = () => {
handleCloseDialog();
setTitle(''); // Reset the title when closing
};

return (
<Dialog
sx={{ '& .MuiDialog-paper': { maxWidth: '400px', minWidth: '300px' } }}
open={open}
onClose={handleClose}
>
<DialogTitle>
<Typography>Create Chartifact Document</Typography>
</DialogTitle>
<DialogContent dividers>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 1 }}>
<TextField
fullWidth
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter document title..."
variant="outlined"
size="small"
/>
<Typography variant="body2" color="text.secondary">
This will create a chartifact document with {chartElements.length} chart{chartElements.length !== 1 ? 's' : ''}.
{chartElements.length > 0 && (
<>
<br />
The document will include:
<br />
• Table data and transformations
<br />
• Sample data for each table
<br />
• Chart references
</>
)}
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
<Button
onClick={handleDownload}
variant="contained"
disabled={!title.trim()}
>
Download
</Button>
</DialogActions>
</Dialog>
);
};

function createChartifact(chartElements: ChartElements[], tables: DictTable[], title: string) {
// Get actual table data from tables array using the IDs
const chartData = chartElements.map(ce => {
const table = tables.find(t => t.id === ce.tableId);

const { chart, conceptShelfItems } = ce.element.props;
const vg = assembleVegaChart(chart.chartType, chart.encodingMap, conceptShelfItems, table?.rows!);

delete vg.data.values;
vg.data.name = table?.id;

vg.padding = 50;

return { table, element: ce.element, chartId: ce.chartId, vg };
}).filter(item => item.table); // Filter out any missing data


// Create more detailed chartifact content
let out = [`${tickWrap('#', 'View this document in the online Chartifact viewer: https://microsoft.github.io/chartifact/view/ \nor with the Chartifact VS Code extension: https://marketplace.visualstudio.com/items?itemName=msrvida.chartifact')}
# Data Formulator session: ${title}
`];

out.push(`This chartifact document contains ${chartData.length} visualization${chartData.length !== 1 ? 's' : ''}.\n`);

chartData.forEach((item, index) => {
out.push(`## Chart ${index + 1}`);
out.push(`**Table:** ${item.table!.displayId || item.table!.id}`);
out.push(`**Chart ID:** ${item.chartId}`);

// Add table info
if (item.table!.derive?.code) {
out.push(`\n**Transformation Code:**`);
out.push(tickWrap('python', item.table!.derive.code));
}

// Add Vega-Lite specification
out.push(`\n**Visualization:**`);
out.push(tickWrap('json vega-lite', JSON.stringify(item.vg, null, 2)));

out.push('\n---\n');
});

// Output unique CSV tables at the end
const uniqueTableIds = [...new Set(chartData.map(item => item.table!.id))];
if (uniqueTableIds.length > 0) {
out.push(`## Data Tables\n\n`);
uniqueTableIds.forEach(tableId => {
const table = tables.find(t => t.id === tableId);
if (table && table.rows && table.rows.length > 0) {
out.push(`### ${table.displayId || table.id}`);
out.push(tickWrap(`csv ${table.id}`, exportTableToDsv(table, ',')));
out.push(tickWrap('json tabulator', JSON.stringify({ dataSourceName: table.id }, null, 2)));
}
});
}

return out.join('\n');
}

function tickWrap(plugin: string, content: string) {
return `\n\n\n\`\`\`${plugin}\n${content}\n\`\`\`\n\n\n`;
}

function generateTitleFromTables(tables: DictTable[]): string {
if (tables.length === 0) return '';

const uniqueTableNames = [...new Set(
tables.map(t => {
const name = t.displayId || t.id || '';
return name ? (name.charAt(0).toUpperCase() + name.slice(1)) : name;
})
)];

if (uniqueTableNames.length <= 3) {
return uniqueTableNames.join(', ');
} else {
return uniqueTableNames.slice(0, 3).join(', ') + '...';
}
}
32 changes: 31 additions & 1 deletion src/views/DataThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Chart, DictTable, EncodingItem, FieldItem, Trigger } from "../component

import DeleteIcon from '@mui/icons-material/Delete';
import AddchartIcon from '@mui/icons-material/Addchart';
import CreateChartifact from '@mui/icons-material/Description';
import StarIcon from '@mui/icons-material/Star';
import SouthIcon from '@mui/icons-material/South';
import TableRowsIcon from '@mui/icons-material/TableRowsOutlined';
Expand All @@ -42,6 +43,7 @@ import CheckIcon from '@mui/icons-material/Check';

import _ from 'lodash';
import { getChartTemplate } from '../components/ChartTemplates';
import { ChartElements, ChartifactDialog } from './ChartifactDialog';

import 'prismjs/components/prism-python' // Language
import 'prismjs/components/prism-typescript' // Language
Expand Down Expand Up @@ -197,13 +199,15 @@ let SingleThreadView: FC<{
leafTable: DictTable;
chartElements: { tableId: string, chartId: string, element: any }[];
usedIntermediateTableIds: string[],
onOpenChartifactDialog: (chartElements: ChartElements[]) => void,
sx?: SxProps
}> = function ({
scrollRef,
threadIdx,
leafTable,
chartElements,
usedIntermediateTableIds, // tables that have been used
onOpenChartifactDialog,
sx
}) {
let tables = useSelector((state: DataFormulatorState) => state.tables);
Expand Down Expand Up @@ -468,6 +472,18 @@ let SingleThreadView: FC<{
}} />
</IconButton>
</Tooltip>
<Tooltip title="download as Chartifact document">
<IconButton aria-label="sdhare" size="small" sx={{ padding: 0.25, '&:hover': {
transform: 'scale(1.2)',
transition: 'all 0.2s ease'
} }}>
<CreateChartifact fontSize="small" sx={{ fontSize: 18 }} color='primary'
onClick={(event) => {
event.stopPropagation();
onOpenChartifactDialog(relevantCharts);
}} />
</IconButton>
</Tooltip>
</ButtonGroup>
</Box>
</Card>
Expand Down Expand Up @@ -783,6 +799,8 @@ export const DataThread: FC<{}> = function ({ }) {
const conceptShelfItems = useSelector((state: DataFormulatorState) => state.conceptShelfItems);

let [threadDrawerOpen, setThreadDrawerOpen] = useState<boolean>(false);
let [chartifactDialogOpen, setChartifactDialogOpen] = useState<boolean>(false);
let [currentChartElements, setCurrentChartElements] = useState<ChartElements[]>([]);

const scrollRef = useRef<null | HTMLDivElement>(null)

Expand All @@ -791,6 +809,11 @@ export const DataThread: FC<{}> = function ({ }) {

const dispatch = useDispatch();

const handleOpenChartifactDialog = (chartElements: ChartElements[]) => {
setCurrentChartElements(chartElements);
setChartifactDialogOpen(true);
};

useEffect(() => {
executeScroll();
}, [threadDrawerOpen])
Expand Down Expand Up @@ -863,7 +886,8 @@ export const DataThread: FC<{}> = function ({ }) {
threadIdx={i}
leafTable={lt}
chartElements={chartElements}
usedIntermediateTableIds={usedIntermediateTableIds}
usedIntermediateTableIds={usedIntermediateTableIds}
onOpenChartifactDialog={handleOpenChartifactDialog}
sx={{
backgroundColor: (i % 2 == 1 ? "rgba(0, 0, 0, 0.03)" : 'white'),
padding: '8px 8px',
Expand Down Expand Up @@ -978,6 +1002,12 @@ export const DataThread: FC<{}> = function ({ }) {

return <Box sx={{ display: 'flex', flexDirection: 'row' }}>
{carousel}
<ChartifactDialog
open={chartifactDialogOpen}
handleCloseDialog={() => setChartifactDialogOpen(false)}
tables={tables}
chartElements={currentChartElements}
/>
</Box>;
}

Expand Down