From d0c849c353be0cc5f7c31e2804c2391dd367f460 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 17 Mar 2025 17:16:33 -0700 Subject: [PATCH 1/6] fix a performance bug on concept shelf --- src/components/ComponentType.tsx | 12 +- src/data/utils.ts | 5 +- src/scss/App.scss | 13 +- src/views/ConceptCard.tsx | 169 ++--------------------- src/views/ConceptShelf.tsx | 209 ++++++++++++----------------- src/views/DataFormulator.tsx | 9 +- src/views/DataView.tsx | 44 ++---- src/views/DerivedDataDialog.tsx | 12 -- src/views/DisambiguationDialog.tsx | 31 +---- src/views/EncodingBox.tsx | 23 +--- src/views/EncodingShelfCard.tsx | 2 +- src/views/EncodingShelfThread.tsx | 2 +- src/views/ModelSelectionDialog.tsx | 12 -- src/views/ReactTable.tsx | 2 +- src/views/ViewUtils.tsx | 2 +- src/views/VisualizationView.tsx | 5 +- 16 files changed, 155 insertions(+), 397 deletions(-) diff --git a/src/components/ComponentType.tsx b/src/components/ComponentType.tsx index 2f2efac5..9bf7e513 100644 --- a/src/components/ComponentType.tsx +++ b/src/components/ComponentType.tsx @@ -20,9 +20,10 @@ export interface FieldItem { type: Type; source: FieldSource; domain: any[]; + tableRef: string; // which table it belongs to, it matters when it's an original field or a derived field + transform?: ConceptTransformation; - tableRef?: string; // which table it comes from, it matters when it's an original field - temporary?: true; + temporary?: true; // the field is temporary, and it will be deleted unless it's saved levels?: {values: any[], reason: string}; // the order in which values in this field would be sorted semanticType?: string; // the semantic type of the object, inferred by the model } @@ -69,13 +70,15 @@ export interface DictTable { // source specifies how the deriviation is done from the source tables, they may be the same, but not necessarily // in fact, right now dict tables are all triggered from charts trigger: Trigger, - } + }; + anchored: boolean; // whether this table is anchored as a persistent table used to derive other tables } export function createDictTable( id: string, rows: any[], derive: {code: string, codeExpl: string, source: string[], dialog: any[], - trigger: Trigger} | undefined = undefined) : DictTable { + trigger: Trigger} | undefined = undefined, + anchored: boolean = false) : DictTable { let names = Object.keys(rows[0]) @@ -85,6 +88,7 @@ export function createDictTable( rows, types: names.map(name => inferTypeFromValueArray(rows.map(r => r[name]))), derive, + anchored } } diff --git a/src/data/utils.ts b/src/data/utils.ts index 61ca9d7b..b0b1a849 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -55,7 +55,7 @@ export const createTableFromText = (title: string, text: string): DictTable | un return createTableFromFromObjectArray(title, values); }; -export const createTableFromFromObjectArray = (title: string, values: any[], derive?: any): DictTable => { +export const createTableFromFromObjectArray = (title: string, values: any[], derive?: any, anchored?: boolean): DictTable => { const len = values.length; let names: string[] = []; let cleanNames: string[] = []; @@ -98,7 +98,8 @@ export const createTableFromFromObjectArray = (title: string, values: any[], der names: columnTable.names(), types: columnTable.names().map(name => (columnTable.column(name) as Column).type), rows: columnTable.objects(), - derive: derive + derive: derive, + anchored: false } }; diff --git a/src/scss/App.scss b/src/scss/App.scss index 40d4dc14..a7083c2f 100644 --- a/src/scss/App.scss +++ b/src/scss/App.scss @@ -91,4 +91,15 @@ h2.view-title { .Resizer.disabled:hover { border-color: transparent; -} \ No newline at end of file +} + +.GroupHeader { + position: sticky; + padding: 4px 8px; + color: rgba(0, 0, 0, 0.6); + font-size: 12px; +} + +.GroupItems { + padding: 0; +} diff --git a/src/views/ConceptCard.tsx b/src/views/ConceptCard.tsx index 86a28bde..5d95190f 100644 --- a/src/views/ConceptCard.tsx +++ b/src/views/ConceptCard.tsx @@ -63,28 +63,16 @@ export interface ConceptCardProps { field: FieldItem, } -export const GroupHeader = styled('div')(({ theme }) => ({ - position: 'sticky', - marginTop: '-8px', - padding: '4px 4px', - color: "rgba(0, 0, 0, 0.6)", - fontSize: "12px", -})); - -export const GroupItems = styled('ul')({ - padding: 0, -}); - const checkConceptIsEmpty = (field: FieldItem) => { return field.name == "" && ((field.source == "derived" && !field.transform?.description && (field.transform as ConceptTransformation).code == "") || (field.source == "custom")) } -export const genFreshDerivedConcept = (parentIDs: string[]) => { +export const genFreshDerivedConcept = (parentIDs: string[], tableRef: string) => { return { id: `concept-${Date.now()}`, name: "", type: "string" as Type, - source: "derived", domain:[], + source: "derived", domain:[], tableRef: tableRef, transform: { parentIDs: parentIDs, code: "", description: ""} } as FieldItem } @@ -130,17 +118,16 @@ export const ConceptCard: FC = function ConceptCard({ field }) let border = "hidden"; const cursorStyle = isDragging ? "grabbing" : "grab"; - let editOption = field.source === "original" ? undefined : ( + let editOption = field.source == "derived" && ( { setEditMode(!editMode) }}> - - ); + ); - let deriveOption = ( + let deriveOption = (field.source == "derived" || field.source == "original") && ( = function ConceptCard({ field }) && f.transform?.parentIDs.includes(field.id)).length > 0) { return } - handleUpdateConcept(genFreshDerivedConcept([field.id])); + handleUpdateConcept(genFreshDerivedConcept([field.id], field.tableRef)); }} > ); - let deleteOption = field.source == "original" ? "" : - f.source == "derived" && f.transform?.parentIDs.includes(field.id)).length > 0} @@ -169,16 +155,13 @@ export const ConceptCard: FC = function ConceptCard({ field }) deleteOption, deriveOption, editOption, - //deleteOption ] - const editModeCard = ( + const editModeCard = field.source == "derived" && ( - {field.source == "derived" ? { setEditMode(false); }} /> : field.source == "custom" ? { setEditMode(false); }} /> : ""} + turnOffEditMode={() => { setEditMode(false); }} /> ); @@ -335,128 +318,6 @@ export const CodeEditor: FC<{ code: string; handleSaveCode: (code: string) => vo } -export const CustomConceptForm: FC = function CustomConceptForm({ concept, handleUpdateConcept, handleDeleteConcept, turnOffEditMode }) { - - const conceptShelfItems = useSelector((state: DataFormulatorState) => state.conceptShelfItems); - - const [name, setName] = useState(concept.name); - const handleNameChange = (event: React.ChangeEvent) => { setName(event.target.value); }; - - const [dtype, setDtype] = useState(concept.name == "" ? "auto" : concept.type as string); - const handleDtypeChange = (event: SelectChangeEvent) => { setDtype(event.target.value); }; - - // if these two fields are changed from other places, update their values - useEffect(() => { setDtype(concept.type) }, [concept.type]); - - let typeList = TypeList - let nameField = ( - f.name == name && f.id != concept.id) ? "this name already exists" : ""} - size="small" onChange={handleNameChange} required error={name == "" || conceptShelfItems.some(f => f.name == name && f.id != concept.id)} - />) - - let typeField = ( - - data type - - - ) - - let cardTopComponents = undefined; - - let childrenConceptIDs = [concept.id]; - while (true) { - let newChildrens = conceptShelfItems.filter(f => f.source == "derived" - && !childrenConceptIDs.includes(f.id) - && f.transform?.parentIDs.some(pid => childrenConceptIDs.includes(pid))) - .map(f => f.id); - if (newChildrens.length == 0) { - break - } - childrenConceptIDs = [...childrenConceptIDs, ...newChildrens]; - } - - cardTopComponents = [ - nameField, - typeField, - ] - - const checkCustomConceptDiff = () => { - let nameTypeNeq = (concept.name != name || concept.type != dtype); - return (nameTypeNeq ); - } - - let saveDisabledMsg = []; - if (name == "" || conceptShelfItems.some(f => f.name == name && f.id != concept.id)) { - saveDisabledMsg.push("concept name is empty") - } - - return ( - - :not(style)': { margin: "4px", /*width: '25ch'*/ }, }} - noValidate - autoComplete="off"> - - {cardTopComponents} - - - f.source == "derived" && f.transform?.parentIDs.includes(concept.id)).length > 0} - onClick={() => { handleDeleteConcept(concept.id); }}> - - - - - - - - - - ); -} export const DerivedConceptForm: FC = function DerivedConceptForm({ concept, handleUpdateConcept, handleDeleteConcept, turnOffEditMode }) { @@ -497,9 +358,6 @@ export const DerivedConceptForm: FC = function DerivedConceptF let dispatch = useDispatch(); - const [collapseCode, setCollapseCode] = useState(true); - const [collapseVisInspector, setCollapseVisInspector] = useState(true); - let [dialogOpen, setDialogOpen] = useState(false); let [codeCandidates, setCodeCandidates] = useState([]); @@ -514,11 +372,8 @@ export const DerivedConceptForm: FC = function DerivedConceptF } }, [transformCode]); - // time stamps used to track functions from server - const [requestTimeStamp, setRequestTimeStamp] = useState(0); const [codeGenInProgress, setCodeGenInProgress] = useState(false); - let typeList = TypeList let nameField = ( = function DerivedConceptF viewExamples = ( illustration of the generated function - + {simpleTableView(transformResult, colNames, conceptShelfItems, 5)} - + ) } diff --git a/src/views/ConceptShelf.tsx b/src/views/ConceptShelf.tsx index be975cd2..0de706dd 100644 --- a/src/views/ConceptShelf.tsx +++ b/src/views/ConceptShelf.tsx @@ -31,7 +31,7 @@ import { OperatorCard } from './OperatorCard'; export const genFreshCustomConcept : () => FieldItem = () => { return { id: `concept-${Date.now()}`, name: "", type: "auto" as Type, domain: [], - description: "", source: "custom", + description: "", source: "custom", tableRef: "custom", } } @@ -43,28 +43,98 @@ export interface ConceptShelfProps { } +export const ConceptGroup: FC<{groupName: string, fields: FieldItem[]}> = function ConceptGroup({groupName, fields}) { + + const focusedTableId = useSelector((state: DataFormulatorState) => state.focusedTableId); + const [expanded, setExpanded] = useState(false); + + useEffect(() => { + if (focusedTableId == groupName) { + setExpanded(true); + } else if (focusedTableId != groupName && groupName != "new fields") { + setExpanded(false); + } + }, [focusedTableId]) + + return + + + setExpanded(!expanded)}> + + {groupName} + + + {expanded ? '▾' : '▸'} + + + + + + {fields.map((field) => ( + + ))} + {fields.length > 5 && !expanded && ( + + + + )} + + {fields.length > 5 && !expanded && ( + + )} + ; +} + export const ConceptShelf: FC = function ConceptShelf() { - let theme = useTheme(); // reference to states const conceptShelfItems = useSelector((state: DataFormulatorState) => state.conceptShelfItems); const focusedTableId = useSelector((state: DataFormulatorState) => state.focusedTableId); const tables = useSelector((state: DataFormulatorState) => state.tables); const charts = useSelector((state: DataFormulatorState) => state.charts); - let [expandedGroups, setExpandedGroups] = useState([]); - let updateExpandedGroup = (groupName: string, isExpanded: boolean) => { - if (isExpanded) { - setExpandedGroups([...expandedGroups, groupName]); - } else { - setExpandedGroups(expandedGroups.filter(g => g != groupName)); - } - } - const dispatch = useDispatch(); - let handleDeleteConcept = (conceptID: string) => dispatch(dfActions.deleteConceptItemByID(conceptID)); - let handleUpdateConcept = (field: FieldItem) => dispatch(dfActions.updateConceptItems(field)); useEffect(() => { let focusedTable = tables.find(t => t.id == focusedTableId); @@ -75,7 +145,7 @@ export const ConceptShelf: FC = function ConceptShelf() { let conceptsToAdd = missingNames.map((name) => { return { id: `concept-${name}-${Date.now()}`, name: name, type: "auto" as Type, - description: "", source: "custom", temporary: true, domain: [], + description: "", source: "custom", tableRef: 'custom', temporary: true, domain: [], } as FieldItem }) dispatch(dfActions.addConceptItems(conceptsToAdd)); @@ -87,48 +157,13 @@ export const ConceptShelf: FC = function ConceptShelf() { // add and delete temporary fields dispatch(dfActions.batchDeleteConceptItemByID(conceptIdsToDelete)); - - // update collapsed groups - let sourceTables = focusedTable.derive?.source || [focusedTable.id]; - setExpandedGroups(sourceTables); - } else { if (tables.length > 0) { dispatch(dfActions.setFocusedTable(tables[0].id)) } } }, [focusedTableId]) - - - let conceptCreatorBtn = ( - - - ); - - // // items for controlling icon creation - // const [fieldSelectorAnchorEl, setFieldSelectorAnchorEl] = React.useState(null); - // const handleOpenFieldSelector = (event: React.MouseEvent) => { - // setFieldSelectorAnchorEl(event.currentTarget); - // }; - // const handleCloseFieldSelector = () => { setFieldSelectorAnchorEl(null); }; - - // define anchor open - // const fieldSelectorOpen = Boolean(fieldSelectorAnchorEl); - // const fieldSelectorId = fieldSelectorOpen ? `conceptCreator` : undefined; - + // group concepts based on types let conceptItemGroups = groupConceptItems(conceptShelfItems); let groupNames = [...new Set(conceptItemGroups.map(g => g.group))] @@ -139,7 +174,6 @@ export const ConceptShelf: FC = function ConceptShelf() { Data Fields - {conceptCreatorBtn} @@ -161,78 +195,7 @@ export const ConceptShelf: FC = function ConceptShelf() { let fields = conceptItemGroups.filter(g => g.group == groupName).map(g => g.field); let isCustomGroup = groupName == "new fields"; - return ( - <> - - - !isCustomGroup && updateExpandedGroup(groupName, !expandedGroups.includes(groupName))}> - - {groupName} - - {!isCustomGroup && ( - - {expandedGroups.includes(groupName) ? '▾' : '▸'} - - )} - - - - - - {(expandedGroups.includes(groupName) || isCustomGroup ? fields : fields.slice(0, 5)).map((field) => ( - - ))} - {!expandedGroups.includes(groupName) && !isCustomGroup && fields.length > 5 && ( - - - - )} - - {!expandedGroups.includes(groupName) && !isCustomGroup && fields.length > 5 && ( - - )} - - ) + return })} diff --git a/src/views/DataFormulator.tsx b/src/views/DataFormulator.tsx index c1aae761..7c4a603f 100644 --- a/src/views/DataFormulator.tsx +++ b/src/views/DataFormulator.tsx @@ -42,11 +42,6 @@ import dfLogo from '../assets/df-logo.png'; import exampleImageTable from "../assets/example-image-table.png"; import { ModelSelectionButton } from './ModelSelectionDialog'; -const MainSplitPane = styled(SplitPane)(({ theme }) => ({ - //height: 'calc(100% - 49px) !important', - //left: '121px !important' -})); - //type AppProps = ConnectedProps; export const DataFormulatorFC = ({ }) => { @@ -89,7 +84,7 @@ export const DataFormulatorFC = ({ }) => { ); const splitPane = ( // @ts-ignore - { {conceptEncodingPanel} {/* */} - ); + ); const fixedSplitPane = ( diff --git a/src/views/DataView.tsx b/src/views/DataView.tsx index cd5095d9..3364f98f 100644 --- a/src/views/DataView.tsx +++ b/src/views/DataView.tsx @@ -39,15 +39,19 @@ export const FreeDataViewFC: FC = function DataView({ $table let derivedFields = conceptShelfItems.filter(f => f.source == "derived" && f.name != ""); - // we only change extTable when conceptShelfItems and extTable changes + // we only change extTable when conceptShelfItems and tables changes let extTables = useMemo(()=>{ - return tables.map(table => { - // try to let table figure out all fields are derivable from the table - let rows = baseTableToExtTable(table.rows, derivedFields, conceptShelfItems); - let extTable = createTableFromFromObjectArray(`${table.id}`, rows, table.derive); - return extTable - }) - }, [tables, conceptShelfItems]) + if (derivedFields.some(f => f.tableRef == focusedTableId)) { + return tables.map(table => { + // try to let table figure out all fields are derivable from the table + let rows = baseTableToExtTable(table.rows, derivedFields, conceptShelfItems); + let extTable = createTableFromFromObjectArray(`${table.id}`, rows, table.derive); + return extTable + }) + } else { + return tables; + } + }, [tables, derivedFields]) useEffect(() => { if(focusedTableId == undefined && tables.length > 0) { @@ -55,22 +59,6 @@ export const FreeDataViewFC: FC = function DataView({ $table } }, [tables]) - // let focusedExtTable = useMemo(() => { - // if (focusedTable == undefined) - // return focusedTable; - - // let toDeriveFields = derivedFields - // .filter(f => !Object.keys((focusedTable as DictTable).rows[0]).includes(f.name)) - // .filter(f => findBaseFields(f, conceptShelfItems).every(f2 => Object.keys((focusedTable as DictTable).rows[0]).includes(f2.name))) - // .filter(f => f.name != "") - // if (toDeriveFields.length == 0) { - // return focusedTable; - // } - // let rows = baseTableToExtTable(JSON.parse(JSON.stringify(focusedTable.rows)), toDeriveFields, conceptShelfItems); - // return createTableFromFromObjectArray(`${focusedTable.title}`, rows); - // }, [conceptShelfItems]) - //console.log(focusedExtTable) - // given a table render the table let renderTableBody = (targetTable: DictTable | undefined) => { const rowData = targetTable ? targetTable.rows.map((r: any, i: number) => ({ ...r, "#rowId": i })) : []; @@ -90,21 +78,17 @@ export const FreeDataViewFC: FC = function DataView({ $table }, ...colDefs] } - // return return } // handle when selection changes const onRangeSelectionChanged = (columns: string[], selected: any[]) => { - // no need to sort it let values = _.uniq(selected); - // dispatch(dfActions.setStagedValues({columns, values})); - // dispatch(dfActions.setStagedValues(_.uniq(valueArray).sort())); }; - let tableToRender = extTables; //focusedTable && !focusedTable.names.every(name => !conceptShelfItems.find(f => f.name == name && f.source == "custom")) ? [baseExtTable, focusedTable] : [baseExtTable]; - + let tableToRender = extTables; + let coreTables = tableToRender.filter(t => t.derive == undefined); let tempTables = tableToRender.filter(t => t.derive); diff --git a/src/views/DerivedDataDialog.tsx b/src/views/DerivedDataDialog.tsx index 1d356c1c..4fd43a87 100644 --- a/src/views/DerivedDataDialog.tsx +++ b/src/views/DerivedDataDialog.tsx @@ -34,18 +34,6 @@ import { CustomReactTable } from './ReactTable'; import DeleteIcon from '@mui/icons-material/Delete'; import SaveIcon from '@mui/icons-material/Save'; -export const GroupHeader = styled('div')(({ theme }) => ({ - position: 'sticky', - top: '-8px', - padding: '4px 4px', - color: "darkgray", - fontSize: "12px", -})); - -export const GroupItems = styled('ul')({ - padding: 0, -}); - export interface DerivedDataDialogProps { chart: Chart, candidateTables: DictTable[], diff --git a/src/views/DisambiguationDialog.tsx b/src/views/DisambiguationDialog.tsx index 62b372c4..7d89b464 100644 --- a/src/views/DisambiguationDialog.tsx +++ b/src/views/DisambiguationDialog.tsx @@ -33,18 +33,6 @@ import { CodexDialogBox } from './ConceptCard'; import { CodeBox } from './VisualizationView'; import { CustomReactTable } from './ReactTable'; -export const GroupHeader = styled('div')(({ theme }) => ({ - position: 'sticky', - top: '-8px', - padding: '4px 4px', - color: "darkgray", - fontSize: "12px", -})); - -export const GroupItems = styled('ul')({ - padding: 0, -}); - export interface DisambiguationDialogProps { conceptName: string; parentIDs: string[]; @@ -201,11 +189,6 @@ export const DisambiguationDialog: FC = function Disa } - - - - - return {setSelectionIdx(idx)}} sx={{minWidth: "280px", maxWidth: "600px", display: "flex", flexGrow: 1, margin: "10px", border: selectionIdx == idx ? "2px solid rgb(2 136 209 / 0.7)": "2px solid rgba(255, 255, 255, 0)"}}> @@ -217,20 +200,20 @@ export const DisambiguationDialog: FC = function Disa '& .MuiFormLabel-root': { fontSize: "inherit" } }} value={idx} control={} label={`candidate-${idx+1}`} /> - - transformation result on sample data - - + + transformation result on sample data + + {/* */} {simpleTableView(codeOutputs[idx], colNames, conceptShelfItems)} - + - + transformation code - + {/* ({ - position: 'sticky', - top: '-8px', - padding: '4px 10px', - fontSize: '10px', - color: 'darkgray', - //backgroundColor: 'rbga(0,0,0,0.6)' - })); - - const GroupItems = styled('ul')({ - padding: 0, - }); + let getChannelDisplay = (channel: Channel) => { if (channel == "x") { @@ -528,7 +517,7 @@ export const EncodingBox: FC = function EncodingBox({ channel, console.log(`about to add ${option}`) let newConept = { id: `concept-${Date.now()}`, name: option, type: "auto" as Type, - description: "", source: "custom", domain: [], + description: "", source: "custom", domain: [], tableRef: "custom", } as FieldItem; dispatch(dfActions.updateConceptItems(newConept)); updateEncProp("fieldID", newConept.id); @@ -579,10 +568,10 @@ export const EncodingBox: FC = function EncodingBox({ channel, } }} renderGroup={(params) => ( -
  • - {params.group} - {params.children} -
  • + + {params.group} + {params.children} + )} renderOption={(props, option) => { let renderOption = (conceptShelfItems.map(f => f.name).includes(option) || option == "...") ? option : `"${option}"`; diff --git a/src/views/EncodingShelfCard.tsx b/src/views/EncodingShelfCard.tsx index 39c0523c..351aa7e0 100644 --- a/src/views/EncodingShelfCard.tsx +++ b/src/views/EncodingShelfCard.tsx @@ -551,7 +551,7 @@ export const EncodingShelfCard: FC = function ({ chartId let conceptsToAdd = missingNames.map((name) => { return { id: `concept-${name}-${Date.now()}`, name: name, type: "auto" as Type, - description: "", source: "custom", temporary: true, domain: [], + description: "", source: "custom", tableRef: "custom", temporary: true, domain: [], } as FieldItem }) dispatch(dfActions.addConceptItems(conceptsToAdd)); diff --git a/src/views/EncodingShelfThread.tsx b/src/views/EncodingShelfThread.tsx index 0bea2e35..5e4e048a 100644 --- a/src/views/EncodingShelfThread.tsx +++ b/src/views/EncodingShelfThread.tsx @@ -313,7 +313,7 @@ export const EncodingShelfThread: FC = function ({ cha let conceptsToAdd = missingNames.map((name) => { return { id: `concept-${name}-${Date.now()}`, name: name, type: "auto" as Type, - description: "", source: "custom", temporary: true, domain: [], + description: "", source: "custom", tableRef: "custom", temporary: true, domain: [], } as FieldItem }); diff --git a/src/views/ModelSelectionDialog.tsx b/src/views/ModelSelectionDialog.tsx index 219fe4fd..0c636bf8 100644 --- a/src/views/ModelSelectionDialog.tsx +++ b/src/views/ModelSelectionDialog.tsx @@ -59,18 +59,6 @@ interface AppConfig { SHOW_KEYS_ENABLED: boolean; } -export const GroupHeader = styled('div')(({ theme }) => ({ - position: 'sticky', - padding: '8px 8px', - marginLeft: '-8px', - color: "rgba(0, 0, 0, 0.6)", - fontSize: "12px", -})); - -export const GroupItems = styled('ul')({ - padding: 0, -}); - export const ModelSelectionButton: React.FC<{}> = ({ }) => { const dispatch = useDispatch(); diff --git a/src/views/ReactTable.tsx b/src/views/ReactTable.tsx index 3cb92e0b..44c6dd5d 100644 --- a/src/views/ReactTable.tsx +++ b/src/views/ReactTable.tsx @@ -71,7 +71,7 @@ export const CustomReactTable: React.FC = ({ rows, column return { } else if (f.source == "custom") { group = "new fields" } else if (f.source == "derived") { - group = findBaseFields(f, conceptShelfItems)[0].tableRef || "custom concepts"; + group = f.tableRef as string; } return {group, field: f} }); diff --git a/src/views/VisualizationView.tsx b/src/views/VisualizationView.tsx index 788053ba..80225b7d 100644 --- a/src/views/VisualizationView.tsx +++ b/src/views/VisualizationView.tsx @@ -229,9 +229,6 @@ const BaseChartCreationMenu: FC<{tableId: string; buttonElement: any}> = functio ; } -export const ChartCreationMenu = styled(BaseChartCreationMenu)({}); - - export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], handleUpdateCandidates: (chartId: string, tables: DictTable[]) => void, }> = function ChartEditorFC({ cachedCandidates, handleUpdateCandidates }) { @@ -405,7 +402,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[],
    - let createNewChartButton = } /> From 51a977a54b6e855a68274955586b51f19fc7a1cc Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 17 Mar 2025 18:39:33 -0700 Subject: [PATCH 2/6] wip anchoring tables --- src/app/dfSlice.tsx | 5 ++ src/data/utils.ts | 10 +-- src/views/DataThread.tsx | 114 +++++++++++++++++++++++-------- src/views/DataView.tsx | 2 +- src/views/TableSelectionView.tsx | 8 +-- 5 files changed, 101 insertions(+), 38 deletions(-) diff --git a/src/app/dfSlice.tsx b/src/app/dfSlice.tsx index bd02f1f3..1e830b4d 100644 --- a/src/app/dfSlice.tsx +++ b/src/app/dfSlice.tsx @@ -335,6 +335,11 @@ export const dataFormulatorSlice = createSlice({ // separate this, so that we only delete on tier of table a time state.charts = state.charts.filter(c => !(c.intermediate && c.intermediate.resultTableId == tableId)); }, + updateTableAnchored: (state, action: PayloadAction<{tableId: string, anchored: boolean}>) => { + let tableId = action.payload.tableId; + let anchored = action.payload.anchored; + state.tables = state.tables.map(t => t.id == tableId ? {...t, anchored} : t); + }, addChallenges: (state, action: PayloadAction<{tableId: string, challenges: { text: string; difficulty: 'easy' | 'medium' | 'hard'; }[]}>) => { state.activeChallenges = [...state.activeChallenges, action.payload]; }, diff --git a/src/data/utils.ts b/src/data/utils.ts index b0b1a849..b023bed7 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -18,7 +18,7 @@ export const loadTextDataWrapper = (title: string, text: string, fileType: strin if (fileType == "text/csv" || fileType == "text/tab-separated-values") { table = createTableFromText(tableName, text); } else if (fileType == "application/json") { - table = createTableFromFromObjectArray(tableName, JSON.parse(text)); + table = createTableFromFromObjectArray(tableName, JSON.parse(text), true); } return table; }; @@ -52,10 +52,10 @@ export const createTableFromText = (title: string, text: string): DictTable | un return row; }); - return createTableFromFromObjectArray(title, values); + return createTableFromFromObjectArray(title, values, true); }; -export const createTableFromFromObjectArray = (title: string, values: any[], derive?: any, anchored?: boolean): DictTable => { +export const createTableFromFromObjectArray = (title: string, values: any[], anchored: boolean, derive?: any): DictTable => { const len = values.length; let names: string[] = []; let cleanNames: string[] = []; @@ -99,7 +99,7 @@ export const createTableFromFromObjectArray = (title: string, values: any[], der types: columnTable.names().map(name => (columnTable.column(name) as Column).type), rows: columnTable.objects(), derive: derive, - anchored: false + anchored: anchored } }; @@ -172,7 +172,7 @@ export const loadBinaryDataWrapper = (title: string, arrayBuffer: ArrayBuffer): const jsonData = XLSX.utils.sheet_to_json(worksheet); // Create a table from the JSON data with sheet name included in the title - const sheetTable = createTableFromFromObjectArray(`${title}-${sheetName}`, jsonData); + const sheetTable = createTableFromFromObjectArray(`${title}-${sheetName}`, jsonData, true); tables.push(sheetTable); } diff --git a/src/views/DataThread.tsx b/src/views/DataThread.tsx index 651e3f0c..1e2b82e4 100644 --- a/src/views/DataThread.tsx +++ b/src/views/DataThread.tsx @@ -15,7 +15,8 @@ import { Tooltip, ButtonGroup, useTheme, - SxProps + SxProps, + Button } from '@mui/material'; import { VegaLite } from 'react-vega' @@ -32,6 +33,7 @@ import AddchartIcon from '@mui/icons-material/Addchart'; import StarIcon from '@mui/icons-material/Star'; import SouthIcon from '@mui/icons-material/South'; import TableRowsIcon from '@mui/icons-material/TableRowsOutlined'; +import AnchorIcon from '@mui/icons-material/Anchor'; import PanoramaFishEyeIcon from '@mui/icons-material/PanoramaFishEye'; import InsightsIcon from '@mui/icons-material/Insights'; @@ -43,7 +45,6 @@ import 'prismjs/components/prism-python' // Language import 'prismjs/components/prism-typescript' // Language import 'prismjs/themes/prism.css'; //Example style, you can use another - import { chartAvailabilityCheck, generateChartSkeleton, getDataTable } from './VisualizationView'; import { TriggerCard } from './EncodingShelfCard'; @@ -78,12 +79,11 @@ let SingleThreadView: FC<{ usedTableIds, // tables that have been used sx }) { - let theme = useTheme(); - let tables = useSelector((state: DataFormulatorState) => state.tables); let charts = useSelector((state: DataFormulatorState) => state.charts); let focusedChartId = useSelector((state: DataFormulatorState) => state.focusedChartId); let focusedTableId = useSelector((state: DataFormulatorState) => state.focusedTableId); + const theme = useTheme(); let focusedChart = charts.find(c => c.id == focusedChartId); @@ -95,6 +95,7 @@ let SingleThreadView: FC<{ let content: any = "" + let originTableIdOfThread = undefined; let tableIdList = [leafTable.id] let triggerCards: any[] = [] let triggers = getTriggers(leafTable, tables); @@ -102,6 +103,10 @@ let SingleThreadView: FC<{ if (leafTable.derive) { let firstNewTableIndex = triggers.findIndex(tg => !usedTableIds.includes(tg.tableId)); firstNewTableIndex = firstNewTableIndex == -1 ? triggers.length : firstNewTableIndex; + + if (firstNewTableIndex - 1 > 0) { + originTableIdOfThread = triggers[0].tableId; + } triggers = firstNewTableIndex > 0 ? triggers.slice(firstNewTableIndex - 1) : triggers; tableIdList = [...triggers.map((trigger) => trigger.tableId), leafTable.id]; @@ -135,8 +140,42 @@ let SingleThreadView: FC<{ }); } - // the thread is focused if the focused chart is in this table - let threadIsFocused = focusedChart && tableIdList.includes(focusedChart.tableRef) && !usedTableIds.includes(focusedChart.tableRef); + + const findTableIdsBetweenAnchors = (tableIds: string[], tables: DictTable[], focusedTableId?: string): string[] => { + if (!focusedTableId) return []; + + // Find the index of the focused table + const focusedIndex = tableIds.indexOf(focusedTableId); + if (focusedIndex === -1) return []; + + // Find anchored tables or boundaries + let startIndex = 0; + let endIndex = tableIds.length - 1; + + // Search backward for an anchored table or start + for (let i = focusedIndex; i >= 0; i--) { + const table = tables.find(t => t.id === tableIds[i]); + if (table?.anchored) { + startIndex = i; + break; + } + } + + // Search forward for an anchored table or end + for (let i = focusedIndex; i < tableIds.length; i++) { + const table = tables.find(t => t.id === tableIds[i]); + if (table?.anchored) { + endIndex = i; + break; + } + } + + // Return the slice of tableIds between anchors + return tableIds.slice(startIndex, endIndex + 1); + }; + + // Find tableIds between anchored tables that include the focused table + let tableIdsBetweenAnchors = findTableIdsBetweenAnchors(originTableIdOfThread ? [originTableIdOfThread, ...tableIdList] : tableIdList, tables, focusedTableId); let tableList = tableIdList.map((tableId, i) => { // filter charts relavent to this @@ -159,17 +198,6 @@ let SingleThreadView: FC<{ // only charts without dependency can be deleted let tableDeleteEnabled = !tables.some(t => t.derive?.trigger.tableId == tableId); - let colloapsedTableBox =
    - - - - - {tableId} - - - -
    ; - let regularTableBox =
    c.chartId == focusedChartId) ? scrollRef : null} style={{ padding: '0px' }}> - - + + { + event.stopPropagation(); + dispatch(dfActions.updateTableAnchored({tableId: tableId, anchored: !table?.anchored})); + }}> + {table?.anchored ? : } + {tableDeleteEnabled && - + { event.stopPropagation(); @@ -210,7 +256,10 @@ let SingleThreadView: FC<{ } - + { event.stopPropagation(); @@ -230,19 +279,18 @@ let SingleThreadView: FC<{ -
    -
    +
    {releventChartElements} @@ -271,6 +319,16 @@ let SingleThreadView: FC<{
    + {originTableIdOfThread && + + {`${originTableIdOfThread}`} + + + } {content}
    diff --git a/src/views/DataView.tsx b/src/views/DataView.tsx index 3364f98f..c9d0aeac 100644 --- a/src/views/DataView.tsx +++ b/src/views/DataView.tsx @@ -45,7 +45,7 @@ export const FreeDataViewFC: FC = function DataView({ $table return tables.map(table => { // try to let table figure out all fields are derivable from the table let rows = baseTableToExtTable(table.rows, derivedFields, conceptShelfItems); - let extTable = createTableFromFromObjectArray(`${table.id}`, rows, table.derive); + let extTable = createTableFromFromObjectArray(`${table.id}`, rows, table.anchored, table.derive); return extTable }) } else { diff --git a/src/views/TableSelectionView.tsx b/src/views/TableSelectionView.tsx index 2d7f8a9b..eb5121d7 100644 --- a/src/views/TableSelectionView.tsx +++ b/src/views/TableSelectionView.tsx @@ -177,7 +177,7 @@ export const TableSelectionDialog: React.FC<{ buttonElement: any }> = function T .then((response) => response.json()) .then((result) => { let tableChallenges : TableChallenges[] = result.map((info: any) => { - let table = createTableFromFromObjectArray(info["name"], JSON.parse(info["snapshot"])) + let table = createTableFromFromObjectArray(info["name"], JSON.parse(info["snapshot"]), true) return {table: table, challenges: info["challenges"], name: info["name"]} }).filter((t : TableChallenges | undefined) => t != undefined); setDatasetPreviews(tableChallenges); @@ -221,7 +221,7 @@ export const TableSelectionDialog: React.FC<{ buttonElement: any }> = function T return response.text() }) .then((text) => { - let fullTable = createTableFromFromObjectArray(tableChallenges.table.id, JSON.parse(text)); + let fullTable = createTableFromFromObjectArray(tableChallenges.table.id, JSON.parse(text), true); if (fullTable) { dispatch(dfActions.loadTable(fullTable)); dispatch(fetchFieldSemanticType(fullTable)); @@ -389,7 +389,7 @@ export const TableURLDialog: React.FC = ({ buttonElement, d let table : undefined | DictTable = undefined; try { let jsonContent = JSON.parse(content); - table = createTableFromFromObjectArray(tableName || 'dataset', jsonContent); + table = createTableFromFromObjectArray(tableName || 'dataset', jsonContent, true); } catch (error) { table = createTableFromText(tableName || 'dataset', content); } @@ -486,7 +486,7 @@ export const TableCopyDialogV2: React.FC = ({ buttonElemen try { let content = JSON.parse(tableStr); - table = createTableFromFromObjectArray(uniqueName, content); + table = createTableFromFromObjectArray(uniqueName, content, true); } catch (error) { table = createTableFromText(uniqueName, tableStr); } From 524027e0d90f5e11f07cedee350ec697b020c193 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Tue, 18 Mar 2025 18:44:45 -0700 Subject: [PATCH 3/6] preparing for data-anchor design --- src/app/dfSlice.tsx | 5 + src/app/utils.tsx | 15 +- src/components/ComponentType.tsx | 2 + src/data/utils.ts | 1 + src/scss/EncodingShelf.scss | 2 +- src/views/ConceptShelf.tsx | 8 +- src/views/DataThread.tsx | 262 +++++++++++++++++++++--------- src/views/DataView.tsx | 2 +- src/views/EncodingBox.tsx | 2 +- src/views/EncodingShelfCard.tsx | 28 ++-- src/views/EncodingShelfThread.tsx | 99 +++++------ src/views/ViewUtils.tsx | 6 +- src/views/VisualizationView.tsx | 2 +- 13 files changed, 270 insertions(+), 164 deletions(-) diff --git a/src/app/dfSlice.tsx b/src/app/dfSlice.tsx index 1e830b4d..221e1453 100644 --- a/src/app/dfSlice.tsx +++ b/src/app/dfSlice.tsx @@ -340,6 +340,11 @@ export const dataFormulatorSlice = createSlice({ let anchored = action.payload.anchored; state.tables = state.tables.map(t => t.id == tableId ? {...t, anchored} : t); }, + updateTableDisplayId: (state, action: PayloadAction<{tableId: string, displayId: string}>) => { + let tableId = action.payload.tableId; + let displayId = action.payload.displayId; + state.tables = state.tables.map(t => t.id == tableId ? {...t, displayId} : t); + }, addChallenges: (state, action: PayloadAction<{tableId: string, challenges: { text: string; difficulty: 'easy' | 'medium' | 'hard'; }[]}>) => { state.activeChallenges = [...state.activeChallenges, action.payload]; }, diff --git a/src/app/utils.tsx b/src/app/utils.tsx index 3afa0493..fcc3a5b2 100644 --- a/src/app/utils.tsx +++ b/src/app/utils.tsx @@ -549,10 +549,21 @@ export const resolveChartFields = (chart: Chart, currentConcepts: FieldItem[], r } export let getTriggers = (leafTable: DictTable, tables: DictTable[]) => { - // recursively find triggers that ends in leafTable + // recursively find triggers that ends in leafTable (if the leaf table is anchored, we will find till the previous table is anchored) let triggers : Trigger[] = []; let t = leafTable; - while(t.derive != undefined) { + while(true) { + + // this is when we find an original table + if (t.derive == undefined) { + break; + } + + // this is when we find an anchored table (which is not the leaf table) + if (t !== leafTable && t.anchored) { + break; + } + let trigger = t.derive.trigger as Trigger; triggers = [trigger, ...triggers]; let parentTable = tables.find(x => x.id == trigger.tableId); diff --git a/src/components/ComponentType.tsx b/src/components/ComponentType.tsx index 9bf7e513..55327c5b 100644 --- a/src/components/ComponentType.tsx +++ b/src/components/ComponentType.tsx @@ -57,6 +57,7 @@ export interface Trigger { export interface DictTable { id: string; // name/id of the table + displayId: string; // display id of the table names: string[]; // column names types: Type[]; // column types rows: any[]; // table content, each entry is a row @@ -84,6 +85,7 @@ export function createDictTable( return { id, + displayId: `${id}`, names, rows, types: names.map(name => inferTypeFromValueArray(rows.map(r => r[name]))), diff --git a/src/data/utils.ts b/src/data/utils.ts index b023bed7..3aba0417 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -95,6 +95,7 @@ export const createTableFromFromObjectArray = (title: string, values: any[], anc return { id: title, + displayId: `${title}`, names: columnTable.names(), types: columnTable.names().map(name => (columnTable.column(name) as Column).type), rows: columnTable.objects(), diff --git a/src/scss/EncodingShelf.scss b/src/scss/EncodingShelf.scss index d399bff7..a4fa23b2 100644 --- a/src/scss/EncodingShelf.scss +++ b/src/scss/EncodingShelf.scss @@ -23,7 +23,7 @@ .encoding-shelf-trigger-card { margin: 6px 2px; - min-width: 160px; + min-width: 80px; } .encoding-shelf-compact { diff --git a/src/views/ConceptShelf.tsx b/src/views/ConceptShelf.tsx index 0de706dd..efb2c916 100644 --- a/src/views/ConceptShelf.tsx +++ b/src/views/ConceptShelf.tsx @@ -85,7 +85,7 @@ export const ConceptGroup: FC<{groupName: string, fields: FieldItem[]}> = functi {fields.map((field) => ( ))} - {fields.length > 5 && !expanded && ( + {fields.length > 6 && !expanded && ( = functi background: 'linear-gradient(to bottom, transparent, white)' } }}> - + )} - {fields.length > 5 && !expanded && ( + {fields.length > 6 && !expanded && ( -
    ); - - let tableCards = activeTableThread.map((tableId) => - - - ); + + }); + let leafTable = tables.find(t => t.id == activeTableThread[activeTableThread.length - 1]) as DictTable; - let triggers = getTriggers(leafTable, tables) //leafTable.derive?.triggers || []; + let triggers = getTriggers(leafTable, tables) + + + console.log('from local data thread') + console.log(triggers[triggers.length - 1]); + let instructionCards = triggers.map((trigger, i) => { let extractActiveFields = (t: Trigger) => { let encodingMap = (charts.find(c => c.id == t.chartRef) as Chart).encodingMap @@ -445,31 +435,25 @@ export const EncodingShelfThread: FC = function ({ cha let fieldsIdentical = _.isEqual(previousActiveFields, currentActiveFields) return - {/* */} - - - {/* - */} - - - {i == triggers.length - 1 && chart.intermediate == undefined ? - - { - reFormulate(triggers[triggers.length - 1]); - }} - >{reformulateRunning ? : } - - : ""} - ; + key={`${trigger.tableId}-trigger-card`} + sx={{padding: 0, display: 'flex'}}> + + + + + {i == triggers.length - 1 && chart.intermediate == undefined ? + + { + reFormulate(triggers[triggers.length - 1]); + }} + >{reformulateRunning ? : } + + : ""} + ; }) let spaceElement = "" //; @@ -516,13 +500,6 @@ export const EncodingShelfThread: FC = function ({ cha backgroundSize: '1px 6px, 3px 100%'}}> , ...instructionCards.slice(cutIndex+1, postInstructEndPoint)], tableList.slice(cutIndex + 1, postInstructEndPoint + 1), spaceElement)} - {/* {w(Array(tableList.length - (cutIndex) - 1).fill( - - - ), - tableList.slice(cutIndex + 1), spaceElement)} */} - {/* {w(tableList.slice(0, tableList.length - 1), instructionList.slice(0, instructionList.length - 1))} */} - {/* */} {endChartCard} diff --git a/src/views/ViewUtils.tsx b/src/views/ViewUtils.tsx index 841c93a4..b4f36bc0 100644 --- a/src/views/ViewUtils.tsx +++ b/src/views/ViewUtils.tsx @@ -133,16 +133,16 @@ export const findBaseFields = (field: FieldItem, conceptShelfItems: FieldItem[]) } } -export const groupConceptItems = (conceptShelfItems: FieldItem[]) => { +export const groupConceptItems = (conceptShelfItems: FieldItem[], tables: DictTable[]) => { // group concepts based on which source table they belongs to return conceptShelfItems.map(f => { let group = "" if (f.source == "original") { - group = f.tableRef as string; + group = tables.find(t => t.id == f.tableRef)?.displayId || f.tableRef; } else if (f.source == "custom") { group = "new fields" } else if (f.source == "derived") { - group = f.tableRef as string; + group = tables.find(t => t.id == f.tableRef)?.displayId || f.tableRef; } return {group, field: f} }); diff --git a/src/views/VisualizationView.tsx b/src/views/VisualizationView.tsx index 80225b7d..6e70936a 100644 --- a/src/views/VisualizationView.tsx +++ b/src/views/VisualizationView.tsx @@ -454,7 +454,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], let chartActionButtons = [ - data: {table.id} + data: {table.displayId || table.id} , ...derivedTableItems, From e1d9d7f138773801ef286baf11ef5212613c66fc Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 20 Mar 2025 18:01:12 -0700 Subject: [PATCH 4/6] performance improvement and fixes, also able to rename tables for better reference --- src/app/dfSlice.tsx | 18 +- src/components/ComponentType.tsx | 1 - src/scss/VisualizationView.scss | 6 - src/views/ConceptShelf.tsx | 4 +- src/views/DataThread.tsx | 323 +++++++++++++++++++++---------- src/views/DataView.tsx | 12 +- src/views/EncodingShelfCard.tsx | 47 +++-- src/views/VisualizationView.tsx | 48 ++--- 8 files changed, 266 insertions(+), 193 deletions(-) diff --git a/src/app/dfSlice.tsx b/src/app/dfSlice.tsx index 221e1453..bdbb911a 100644 --- a/src/app/dfSlice.tsx +++ b/src/app/dfSlice.tsx @@ -142,9 +142,11 @@ let deleteChartsRoutine = (state: DataFormulatorState, chartIds: string[]) => { state.activeThreadChartId = activeThreadChartId; let unrefedDerivedTableIds = getUnrefedDerivedTableIds(state); - state.tables = state.tables.filter(t => !unrefedDerivedTableIds.includes(t.id)); + let tableIdsToDelete = state.tables.filter(t => !t.anchored && unrefedDerivedTableIds.includes(t.id)).map(t => t.id); + + state.tables = state.tables.filter(t => !tableIdsToDelete.includes(t.id)); // remove intermediate charts that lead to this table - state.charts = state.charts.filter(c => !(c.intermediate && unrefedDerivedTableIds.includes(c.intermediate.resultTableId))); + state.charts = state.charts.filter(c => !(c.intermediate && tableIdsToDelete.includes(c.intermediate.resultTableId))); } export const fetchFieldSemanticType = createAsyncThunk( @@ -381,18 +383,6 @@ export const dataFormulatorSlice = createSlice({ } }) }, - updateChartScaleFactor: (state, action: PayloadAction<{chartId: string, scaleFactor: number}>) => { - let chartId = action.payload.chartId; - let scaleFactor = action.payload.scaleFactor; - - state.charts = state.charts.map(chart => { - if (chart.id == chartId) { - return { ...chart, scaleFactor: scaleFactor }; - } else { - return chart; - } - }) - }, deleteChartById: (state, action: PayloadAction) => { let chartId = action.payload; deleteChartsRoutine(state, [chartId]); diff --git a/src/components/ComponentType.tsx b/src/components/ComponentType.tsx index 55327c5b..83d3c19e 100644 --- a/src/components/ComponentType.tsx +++ b/src/components/ComponentType.tsx @@ -100,7 +100,6 @@ export type Chart = { encodingMap: EncodingMap, tableRef: string, saved: boolean, - scaleFactor?: number, intermediate?: Trigger // whether this chart is only an intermediate chart (e.g., only used as a spec for transforming tables) } diff --git a/src/scss/VisualizationView.scss b/src/scss/VisualizationView.scss index 2fdaafa9..ac2e75ef 100644 --- a/src/scss/VisualizationView.scss +++ b/src/scss/VisualizationView.scss @@ -152,12 +152,6 @@ $accelerate-ease: cubic-bezier(0.4, 0.0, 1, 1); animation: appear 0.5s ease-out; } - .focused-vega-thumbnail { - //background-color: blue; - box-shadow: 0 0 5px rgba(33,33,33,.3); - z-index: 1; - } - .vega-thumbnail:hover { transform: translateY(1px); box-shadow: 0 0 3px rgba(33,33,33,.2); diff --git a/src/views/ConceptShelf.tsx b/src/views/ConceptShelf.tsx index efb2c916..35b691a2 100644 --- a/src/views/ConceptShelf.tsx +++ b/src/views/ConceptShelf.tsx @@ -46,10 +46,12 @@ export interface ConceptShelfProps { export const ConceptGroup: FC<{groupName: string, fields: FieldItem[]}> = function ConceptGroup({groupName, fields}) { const focusedTableId = useSelector((state: DataFormulatorState) => state.focusedTableId); + const tables = useSelector((state: DataFormulatorState) => state.tables); const [expanded, setExpanded] = useState(false); useEffect(() => { - if (focusedTableId == groupName) { + let focusedTable = tables.find(t => t.id == focusedTableId); + if (focusedTableId == groupName || focusedTable?.derive?.source.includes(groupName)) { setExpanded(true); } else if (focusedTableId != groupName && groupName != "new fields") { setExpanded(false); diff --git a/src/views/DataThread.tsx b/src/views/DataThread.tsx index 0f6f24e0..4ca9dfc5 100644 --- a/src/views/DataThread.tsx +++ b/src/views/DataThread.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { FC, useEffect, useRef, useState } from 'react'; +import React, { FC, useEffect, useMemo, useRef, useState, useCallback, memo } from 'react'; import { Box, @@ -69,24 +69,27 @@ let buildChartCard = (chartElement: { tableId: string, chartId: string, element: const EditableTableName: FC<{ initialValue: string, tableId: string, - dispatch: any -}> = ({ initialValue, tableId, dispatch }) => { + handleUpdateTableDisplayId: (tableId: string, displayId: string) => void, + nonEditingSx?: SxProps +}> = ({ initialValue, tableId, handleUpdateTableDisplayId, nonEditingSx }) => { const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(initialValue); - const handleSubmit = () => { - if (inputValue !== initialValue) { - dispatch(dfActions.updateTableDisplayId({ - tableId: tableId, - displayId: inputValue - })); + const handleSubmit = (e?: React.MouseEvent | React.KeyboardEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + if (inputValue.trim() !== '') { // Only update if input is not empty + handleUpdateTableDisplayId(tableId, inputValue); + setIsEditing(false); } - setIsEditing(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - handleSubmit(); + handleSubmit(e); } else if (e.key === 'Escape') { setInputValue(initialValue); setIsEditing(false); @@ -95,29 +98,31 @@ const EditableTableName: FC<{ if (!isEditing) { return ( - { - event.stopPropagation(); - setIsEditing(true); - }} - sx={{ - textAlign: 'center', - color: 'rgba(0,0,0,0.5)', - maxWidth: '100px', - wordWrap: 'break-word', - whiteSpace: 'normal', - ml: 0.25, - padding: '2px', - '&:hover': { - backgroundColor: 'rgba(0,0,0,0.04)', - borderRadius: '2px', - cursor: 'pointer' - } - }} - > - {initialValue} - + + { + event.stopPropagation(); + setIsEditing(true); + }} + sx={{ + ...nonEditingSx, + fontSize: 'inherit', + minWidth: '60px', + maxWidth: '100px', + wordWrap: 'break-word', + whiteSpace: 'normal', + ml: 0.25, + padding: '2px', + '&:hover': { + backgroundColor: 'rgba(0,0,0,0.04)', + borderRadius: '2px', + cursor: 'pointer' + } + }} + > + {initialValue} + + ); } @@ -137,18 +142,21 @@ const EditableTableName: FC<{ onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} autoFocus - variant="outlined" + variant="filled" size="small" - onBlur={() => { - setInputValue(initialValue); - setIsEditing(false); + onBlur={(e) => { + // Only reset if click is not on the submit button + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setInputValue(initialValue); + setIsEditing(false); + } }} sx={{ - '& .MuiOutlinedInput-root': { + '& .MuiFilledInput-root': { fontSize: 'inherit', padding: 0, '& input': { - padding: '2px 24px 2px 8px', // Make room for the button + padding: '2px 24px 2px 8px', width: '80px', } } @@ -156,12 +164,16 @@ const EditableTableName: FC<{ /> { + e.preventDefault(); // Prevent blur from firing before click + }} + onClick={(e) => handleSubmit(e)} sx={{ position: 'absolute', right: 2, padding: '2px', minWidth: 'unset', + zIndex: 1, '& .MuiSvgIcon-root': { fontSize: '0.8rem' } @@ -192,6 +204,14 @@ let SingleThreadView: FC<{ let charts = useSelector((state: DataFormulatorState) => state.charts); let focusedChartId = useSelector((state: DataFormulatorState) => state.focusedChartId); let focusedTableId = useSelector((state: DataFormulatorState) => state.focusedTableId); + + let handleUpdateTableDisplayId = (tableId: string, displayId: string) => { + dispatch(dfActions.updateTableDisplayId({ + tableId: tableId, + displayId: displayId + })); + } + const theme = useTheme(); let focusedChart = charts.find(c => c.id == focusedChartId); @@ -210,8 +230,6 @@ let SingleThreadView: FC<{ let highlightedTableIds: string[] = [leafTable.id]; - console.log(`leafTable: ${leafTable.id}; triggers: ${triggers.map(t => t.tableId)}`); - if (leafTable.derive) { // find the first table that belongs to this thread, it should not be an intermediate table that has appeared in previous threads @@ -269,14 +287,46 @@ let SingleThreadView: FC<{ if (tableId == leafTable.id && leafTable.anchored && tableIdList.length > 1) { let table = tables.find(t => t.id == tableId); return - + { + event.stopPropagation(); + dispatch(dfActions.setFocusedTable(tableId)); + + // Find and set the first chart associated with this table + let firstRelatedChart = charts.find((c: Chart) => c.tableRef == tableId && c.intermediate == undefined) + || charts.find((c: Chart) => c.tableRef == tableId); + + if (firstRelatedChart) { + if (firstRelatedChart.intermediate == undefined) { + dispatch(dfActions.setFocusedChart(firstRelatedChart.id)); + } + } + }} + > - + + {table?.displayId || tableId} + @@ -342,16 +392,49 @@ let SingleThreadView: FC<{ event.stopPropagation(); dispatch(dfActions.updateTableAnchored({tableId: tableId, anchored: !table?.anchored})); }}> - {table?.anchored ? : } + + {/* Wrapper span needed for disabled IconButton tooltip */} + t.derive?.trigger.tableId == tableId)} + onClick={(event) => { + event.stopPropagation(); + dispatch(dfActions.updateTableAnchored({tableId: tableId, anchored: !table?.anchored})); + }}> + {table?.anchored ? + : + + } + + + - : {table?.displayId || tableId} + }}>{table?.displayId || tableId}} @@ -444,6 +527,64 @@ let SingleThreadView: FC<{ } +const ChartElement = memo<{ + chart: Chart, + assembledSpec: any, + table: any, + chartSynthesisInProgress: string[], + isSaved?: boolean, + onChartClick: (chartId: string, tableId: string) => void, + onDelete: (chartId: string) => void +}>(({ chart, assembledSpec, table, chartSynthesisInProgress, isSaved, onChartClick, onDelete }) => { + const id = `data-thread-chart-Element-${chart.id}`; + + return ( + onChartClick(chart.id, table.id)} + className="vega-thumbnail-box" + style={{ width: "100%", position: "relative", cursor: "pointer !important" }} + > + + {isSaved && + + } + {chartSynthesisInProgress.includes(chart.id) && + + } + + + { + event.stopPropagation(); + onDelete(chart.id); + }} + > + + + + + + + + + + ); +}); + export const DataThread: FC<{}> = function ({ }) { let tables = useSelector((state: DataFormulatorState) => state.tables); @@ -473,23 +614,17 @@ export const DataThread: FC<{}> = function ({ }) { // when there is no result and synthesis is running, just show the waiting panel // // we don't always render it, so make this a function to enable lazy rendering - let chartElements = charts.filter(chart => !chart.intermediate).map((chart, index) => { - const id = `data-thread-chart-Element-${chart.id}`; + const handleChartClick = useCallback((chartId: string, tableId: string) => { + dispatch(dfActions.setFocusedChart(chartId)); + dispatch(dfActions.setFocusedTable(tableId)); + }, [dispatch]); - let table = getDataTable(chart, tables, charts, conceptShelfItems); + let chartElements = useMemo(() => charts.filter(chart => !chart.intermediate).map((chart) => { + const table = getDataTable(chart, tables, charts, conceptShelfItems); let toDeriveFields = derivedFields.filter(f => f.name != "").filter(f => findBaseFields(f, conceptShelfItems).every(f2 => table.names.includes(f2.name))) let extTable = baseTableToExtTable(JSON.parse(JSON.stringify(table.rows)), toDeriveFields, conceptShelfItems); - let chartTemplate = getChartTemplate(chart.chartType); - - let setIndexFunc = () => { - //let focusedIndex = index; - dispatch(dfActions.setFocusedChart(chart.id)); - dispatch(dfActions.setFocusedTable(table.id)); - //this.setState({focusedIndex, focusUpdated: true}); - } - if (chart.chartType == "Auto") { let element = @@ -501,9 +636,13 @@ export const DataThread: FC<{}> = function ({ }) { if (!available || chart.chartType == "Table") { - let element = >> chart = ", chart) + + let chartTemplate = getChartTemplate(chart.chartType); + + let element = handleChartClick(chart.id, table.id)} sx={{ display: "flex", backgroundColor: "rgba(0,0,0,0.01)", position: 'relative', //border: "0.5px dashed lightgray", @@ -563,48 +702,18 @@ export const DataThread: FC<{}> = function ({ }) { "axis": { "labelLimit": 30 } } - const element = - - - {chart.saved ? - - : ""} - {chartSynthesisInProgress.includes(chart.id) ? - - : ''} - - - { - event.stopPropagation(); - dispatch(dfActions.deleteChartById(chart.id)); - }}> - - - - - - - ; + const element = dispatch(dfActions.deleteChartById(chartId))} + />; return { chartId: chart.id, tableId: table.id, element }; - }) + }), [charts, tables, conceptShelfItems, chartSynthesisInProgress, handleChartClick]); // anchors are considered leaf tables to simplify the view @@ -650,7 +759,7 @@ export const DataThread: FC<{}> = function ({ }) { let usedIntermediateTableIds = leafTables.slice(0, i) .map(x => [ ...getTriggers(x, tables).map(y => y.tableId) || []]).flat(); return ; @@ -89,8 +90,8 @@ export const FreeDataViewFC: FC = function DataView({ $table let tableToRender = extTables; - let coreTables = tableToRender.filter(t => t.derive == undefined); - let tempTables = tableToRender.filter(t => t.derive); + let coreTables = tableToRender.filter(t => t.derive == undefined || t.anchored); + let tempTables = tableToRender.filter(t => t.derive && !t.anchored); let genTableLink = (t: DictTable) => = function DataView({ $table return ( - {/* - {coreTables.map((t, i) => [i > 0 ? : "", genTableLink(t)])} - */} + + {coreTables.map(t => genTableLink(t))} {/* */} - + {tempTables.map(t => genTableLink(t))} diff --git a/src/views/EncodingShelfCard.tsx b/src/views/EncodingShelfCard.tsx index 355f1dac..f007f8f4 100644 --- a/src/views/EncodingShelfCard.tsx +++ b/src/views/EncodingShelfCard.tsx @@ -99,7 +99,6 @@ export const TriggerCard: FC<{className?: string, trigger: Trigger, hideFields?: const charts = useSelector((state: DataFormulatorState) => state.charts); let fieldItems = useSelector((state: DataFormulatorState) => state.conceptShelfItems); - const focusedChartId = useSelector((state: DataFormulatorState) => state.focusedChartId); const dispatch = useDispatch(); @@ -204,15 +203,18 @@ export const MiniTriggerCard: FC<{className?: string, trigger: Trigger, hideFiel } // Add this component before EncodingShelfCard -const ActionTableSelector: FC<{ - actionTableIds: string[], +const UserActionTableSelector: FC<{ + requiredActionTableIds: string[], + userSelectedActionTableIds: string[], tables: DictTable[], - updateActionTableIds: (tableIds: string[]) => void, + updateUserSelectedActionTableIds: (tableIds: string[]) => void, requiredTableIds?: string[] -}> = ({ actionTableIds, tables, updateActionTableIds, requiredTableIds = [] }) => { +}> = ({ requiredActionTableIds, userSelectedActionTableIds, tables, updateUserSelectedActionTableIds, requiredTableIds = [] }) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); + let actionTableIds = [...requiredActionTableIds, ...userSelectedActionTableIds.filter(id => !requiredActionTableIds.includes(id))]; + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -223,7 +225,7 @@ const ActionTableSelector: FC<{ const handleTableSelect = (table: DictTable) => { if (!actionTableIds.includes(table.id)) { - updateActionTableIds([...actionTableIds, table.id]); + updateUserSelectedActionTableIds([...userSelectedActionTableIds, table.id]); } handleClose(); }; @@ -241,7 +243,7 @@ const ActionTableSelector: FC<{ return ( t.id == tableId)?.displayId} size="small" sx={{ height: 16, @@ -255,7 +257,7 @@ const ActionTableSelector: FC<{ } }} deleteIcon={} - onDelete={isRequired ? undefined : () => updateActionTableIds(actionTableIds.filter(id => id !== tableId))} + onDelete={isRequired ? undefined : () => updateUserSelectedActionTableIds(actionTableIds.filter(id => id !== tableId))} /> ); })} @@ -293,7 +295,7 @@ const ActionTableSelector: FC<{ alignItems: 'center' }} > - {table.id} + {table.displayId} ); }) @@ -331,12 +333,12 @@ export const EncodingShelfCard: FC = function ({ chartId // Add this state - const [actionTableIds, setActionTableIds] = useState([]); + const [userSelectedActionTableIds, setUserSelectedActionTableIds] = useState([]); // Update the handler to use state - const handleActionTableChange = (newTableIds: string[]) => { - setActionTableIds(newTableIds); + const handleUserSelectedActionTableChange = (newTableIds: string[]) => { + setUserSelectedActionTableIds(newTableIds); }; let encodingBoxGroups = Object.entries(ChannelGroups) @@ -365,17 +367,11 @@ export const EncodingShelfCard: FC = function ({ chartId // this is the base tables that will be used to derive the new data // this is the bare minimum tables that are required to derive the new data, based fields that will be used let requiredActionTables = selectBaseTables(activeFields, conceptShelfItems, tables, currentTable); + let actionTableIds = [ + ...requiredActionTables.map(t => t.id), + ...userSelectedActionTableIds.filter(id => !requiredActionTables.map(t => t.id).includes(id)) + ]; - useEffect(() => { - setActionTableIds([ - ...requiredActionTables.map(t => t.id), - ...actionTableIds - .filter(id => !requiredActionTables.map(t => t.id).includes(id)) - .filter(id => tables.find(t => t.id == id)?.anchored) - .filter(id => requiredActionTables.some(t => !t.derive?.source.includes(id))) - ]); - }, [requiredActionTables]); - let deriveNewData = (overrideTableId?: string) => { let mode = 'formulate'; @@ -720,10 +716,11 @@ export const EncodingShelfCard: FC = function ({ chartId let channelComponent = ( - {existMultiplePossibleBaseTables && t.id)} + userSelectedActionTableIds={userSelectedActionTableIds} tables={tables.filter(t => t.derive === undefined || t.anchored)} - updateActionTableIds={handleActionTableChange} + updateUserSelectedActionTableIds={handleUserSelectedActionTableChange} requiredTableIds={requiredActionTables.map(t => t.id)} />} diff --git a/src/views/VisualizationView.tsx b/src/views/VisualizationView.tsx index 6e70936a..0390fd84 100644 --- a/src/views/VisualizationView.tsx +++ b/src/views/VisualizationView.tsx @@ -261,7 +261,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], let [collapseEditor, setCollapseEditor] = useState(false); - let scaleFactor = focusedChart.scaleFactor || 1; + const [localScaleFactor, setLocalScaleFactor] = useState(1); useEffect(() => { setFocusUpdated(true); @@ -321,7 +321,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], if (comp) { const { width, height } = comp.getBoundingClientRect(); // console.log(`main chart; width = ${width} height = ${height}`) - comp?.setAttribute("style", `width: ${width * scaleFactor}px; height: ${height * scaleFactor}px;`); + comp?.setAttribute("style", `width: ${width * localScaleFactor}px; height: ${height * localScaleFactor}px;`); } } @@ -330,7 +330,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], if (comp) { const { width, height } = comp.getBoundingClientRect(); // console.log(`main chart; width = ${width} height = ${height}`) - comp?.setAttribute("style", `width: ${width * scaleFactor}px; height: ${height * scaleFactor}px;`); + comp?.setAttribute("style", `width: ${width * localScaleFactor}px; height: ${height * localScaleFactor}px;`); } } @@ -557,6 +557,10 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], ] } else { + + let transformationIndicatorText = table.derive?.source ? + `${table.derive.source.map(s => tables.find(t => t.id === s)?.displayId || s).join(", ")} → ${table.displayId || table.id}` : ""; + focusedComponent = [ - {/* - { setChatDialogOpen(!chatDialogOpen) }}> - - - - - { setCodeViewOpen(false); setCodeExplViewOpen(true) }}> - - */} {setCodeViewOpen(false)}} color='primary' aria-label="delete"> @@ -590,7 +583,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], border: "1px solid rgba(33, 33, 33, 0.1)"}}> - Data transformation code ({table.derive?.source} → {table.id}) + Data transformation code ({transformationIndicatorText}) @@ -604,17 +597,6 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], - {/* - { setChatDialogOpen(!chatDialogOpen) }}> - - - - - { setCodeViewOpen(true); setCodeExplViewOpen(false) }}> - - */} {setCodeExplViewOpen(false)}} color='primary' aria-label="delete"> @@ -624,7 +606,7 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], border: "1px solid rgba(33, 33, 33, 0.1)"}}> - Data transformation explanation ({table.derive?.source} → {table.id}) + Data transformation explanation ({transformationIndicatorText}) @@ -672,19 +654,19 @@ export const ChartEditorFC: FC<{ cachedCandidates: DictTable[], let chartResizer = - { - dispatch(dfActions.updateChartScaleFactor({ chartId: focusedChart.id, scaleFactor: scaleFactor - 0.1 })) + { + setLocalScaleFactor(prev => Math.max(scaleMin, prev - 0.1)); }}> { - dispatch(dfActions.updateChartScaleFactor({chartId: focusedChart.id, scaleFactor: newValue as number})) + value={localScaleFactor} onChange={(event: Event, newValue: number | number[]) => { + setLocalScaleFactor(newValue as number); }} /> - = scaleMax} onClick={() => { - dispatch(dfActions.updateChartScaleFactor({ chartId: focusedChart.id, scaleFactor: scaleFactor + 0.1 })) + = scaleMax} onClick={() => { + setLocalScaleFactor(prev => Math.min(scaleMax, prev + 0.1)); }}> From 45b23b8038eb524a54312697b784a796589f7f25 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 20 Mar 2025 19:16:08 -0700 Subject: [PATCH 5/6] update descriptions --- README.md | 5 +++++ pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 031be5b2..6002b77f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ Transform data and create rich visualizations iteratively with AI 🪄. Try Data ## News 🔥🔥🔥 +- [03-20-2025] Data Formulator 0.1.7 + - Anchor an intermediate dataset, so that followup data analysis are built on top of the anchored data, not the original one. It could be handy when you clean a data, and only want to use the cleaned data for followup analysis; when you want to create a subset from the original data or join multiple data together, and then focus your analysis. The AI agent will be less likely to get confused and work faster. ⚡️ + - Fixed some performane issues from the UI (let us know if it is smoother). + - Don't forget to update Data Formulator to test it out. + - [02-20-2025] Data Formulator 0.1.6 released! - Now supports working with multiple datasets at once! Tell Data Formulator which data tables you would like to use in the encoding shelf, and it will figure out how to join the tables to create a visualization to answer your question. 🪄 - Checkout the demo at [[https://github.com/microsoft/data-formulator/releases/tag/0.1.6]](https://github.com/microsoft/data-formulator/releases/tag/0.1.6). diff --git a/pyproject.toml b/pyproject.toml index 01378920..59923458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "data_formulator" -version = "0.1.6.2" +version = "0.1.7" requires-python = ">=3.9" authors = [ From 9447da52ffa9b120e796f4953dd00c2b892e2b7b Mon Sep 17 00:00:00 2001 From: Chenglong Wang <93549116+Chenglong-MS@users.noreply.github.com> Date: Thu, 20 Mar 2025 19:18:44 -0700 Subject: [PATCH 6/6] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6002b77f..aaf0826c 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ Transform data and create rich visualizations iteratively with AI 🪄. Try Data ## News 🔥🔥🔥 -- [03-20-2025] Data Formulator 0.1.7 - - Anchor an intermediate dataset, so that followup data analysis are built on top of the anchored data, not the original one. It could be handy when you clean a data, and only want to use the cleaned data for followup analysis; when you want to create a subset from the original data or join multiple data together, and then focus your analysis. The AI agent will be less likely to get confused and work faster. ⚡️ - - Fixed some performane issues from the UI (let us know if it is smoother). - - Don't forget to update Data Formulator to test it out. +- [03-20-2025] Data Formulator 0.1.7: Anchoring ⚓︎ + - Anchor an intermediate dataset, so that followup data analysis are built on top of the anchored data, not the original one. + - It is handy when: clean a data and work with only the cleaned data; create a subset from the original data or join multiple data, and then focus your analysis from there. The AI agent will be less likely to get confused and work faster. ⚡️⚡️ + - Don't forget to update Data Formulator to test it out! - [02-20-2025] Data Formulator 0.1.6 released! - Now supports working with multiple datasets at once! Tell Data Formulator which data tables you would like to use in the encoding shelf, and it will figure out how to join the tables to create a visualization to answer your question. 🪄