Skip to content

Commit

Permalink
add time axis
Browse files Browse the repository at this point in the history
  • Loading branch information
keckelt committed Mar 25, 2024
1 parent 4bc2ea3 commit e87c399
Show file tree
Hide file tree
Showing 8 changed files with 977 additions and 38 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@
"d3-selection": "^3.0.0",
"html-react-parser": "^4.0.0",
"monaco-editor": "^0.41.0",
"react-vega": "^7.6.0",
"react-xarrows": "^2.0.2",
"tabletojson": "^4.0.1",
"vega": "^5.28.0",
"vega-lite": "^5.17.0",
"zustand": "^4.3.8"
},
"devDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion src/LoopsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import { create } from 'zustand';
type LoopsState = {
activeCellID: string | undefined;
activeCellTop: number | undefined;
stateData: Map<number, { date: Date; cellExecutions: number; isVisible: boolean }>;

setActiveCell: (cellID: string | undefined, cellTop: number | undefined) => void;
clearActiveCell: () => void;
clearStateData: () => void;
};

export const useLoopsStore = create<LoopsState>(set => ({
activeCellID: undefined,
activeCellTop: undefined,
stateData: new Map(),

setActiveCell: (cellID, cellTop) => set(state => ({ activeCellID: cellID, activeCellTop: cellTop })),
clearActiveCell: () => set({ activeCellID: undefined, activeCellTop: undefined })
clearActiveCell: () => set({ activeCellID: undefined, activeCellTop: undefined }),
clearStateData: () => set({ stateData: new Map() })
}));
18 changes: 16 additions & 2 deletions src/Overview/State.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CodeCell } from './Cells/CodeCell';
import { DeletedCell } from './Cells/DeletedCell';
import { MarkdownCell } from './Cells/MarkDownCell';
import { useXarrow } from 'react-xarrows';
import { useIsVisible } from '../useIsVisible';

const useStyles = createStyles((theme, _params) => ({
header: {
Expand Down Expand Up @@ -300,6 +301,16 @@ export function State({
const activeCellTop = useLoopsStore(state => state.activeCellTop);
const stateScrollerRef = useRef<HTMLDivElement>(null);

const isVisible = useIsVisible(stateScrollerRef);
useLoopsStore.setState(prev => ({
...prev,
stateData: new Map(prev.stateData).set(stateNo, {
cellExecutions: Array.from(cellExecutions.values()).reduce((acc, cellExec) => acc + cellExec.count, 0),
date: timestamp,
isVisible: isVisible
})
}));

useEffect(
() => {
const scrollToElement = () => {
Expand Down Expand Up @@ -465,6 +476,9 @@ export function State({
})}
>
<header className={cx(classes.header, classes.dashedBorder)}>
<Center>
<Avatar.Group spacing={fullWidth ? 8 : 12}>{avatars}</Avatar.Group>
</Center>
<Center>
<ActionIcon onClick={toggleFullwidth} title={fullWidth ? 'collapse' : 'expand'}>
{fullWidth ? <IconArrowsDiff /> : <IconArrowsHorizontal />}
Expand All @@ -478,7 +492,7 @@ export function State({
<div style={{ height: '100vh' }}></div>
</div>
</div>
<div className={cx(classes.versionSplit)}>
{/* <div className={cx(classes.versionSplit)}>
{!fullWidth ? (
<>
<div>v{stateNo + 1}</div>
Expand All @@ -499,7 +513,7 @@ export function State({
</Center>
</>
)}
</div>
</div> */}
</div>
);
}
28 changes: 10 additions & 18 deletions src/Overview/StateList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LoopsActiveCellMetaDataKey, LoopsStateMetaDataKey, LoopsUserMetaDataKey
import { State } from './State';
import { logTimes } from '../util';
import Xarrow, { Xwrapper } from 'react-xarrows';
import { TimeAxis } from './TimeAxis';

const useStyles = createStyles((theme, _params) => ({
stateList: {
Expand Down Expand Up @@ -211,7 +212,6 @@ export function StateList({ nbTracker, labShell }: IStateListProps): JSX.Element
logTimes && console.timeEnd(step);
step = 'create states';
logTimes && console.time(step);
const stateTimes: any[] = [];

const states = statesFiltered.map((state, i, statesArray) => {
const previousLastState = i - 1 >= 0 ? statesArray[i - 1].state : undefined;
Expand All @@ -221,16 +221,6 @@ export function StateList({ nbTracker, labShell }: IStateListProps): JSX.Element
//create a map of cell Ids to execution counts

const previousStateNo = i - 1 >= 0 ? statesArray[i - 1].stateNo : undefined;
stateTimes.push({
stateNo: thisLastState.stateNo,
timestamp: thisLastState.node.createdOn,
date: new Date(thisLastState.node.createdOn),
// Sum up count from all cellExecutions
cellExecutions: Array.from(thisLastState.cellExecutions.values()).reduce(
(acc, cellExec) => acc + cellExec.count,
0
)
});

Array.from(thisLastState.cellExecutions.keys())
.filter(cellId => previouSCellExecutions?.has(cellId)) // did it exist, so we can connect?
Expand Down Expand Up @@ -270,14 +260,16 @@ export function StateList({ nbTracker, labShell }: IStateListProps): JSX.Element
logTimes && console.timeEnd(step);
logTimes && console.timeEnd('create states total');

// console.log('stateTimes', stateTimes);
return (
<div ref={stateListRef} className={classes.stateList} id="Statelist">
<Xwrapper>
{states}
{lines}
</Xwrapper>
</div>
<>
<div ref={stateListRef} className={classes.stateList} id="Statelist">
<Xwrapper>
{states}
{lines}
</Xwrapper>
</div>
<TimeAxis />
</>
);
}

Expand Down
147 changes: 147 additions & 0 deletions src/Overview/TimeAxis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useEffect, useRef } from 'react';
import { Vega, VisualizationSpec } from 'react-vega';
import { throttleAndDebounce } from '../util';
import { useLoopsStore } from '../LoopsStore';

export function TimeAxis(): JSX.Element {
const containerRef = useRef(null);

const stateData = useLoopsStore(state => state.stateData);
const executionData: { date: Date; cellExecutions: number; isVisible: boolean }[] = Array.from(stateData.values());

useEffect(() => {
// call the resize event at most every 100ms and debounce it
const delayedResize = throttleAndDebounce(
() => {
window.dispatchEvent(new Event('resize')); // trigger a resize event to update the vega-lite chart width
},
100,
100
); // throttle time and debounce time in ms

const resizeObserver = new ResizeObserver(delayedResize);
const observedContainer = containerRef.current;

if (observedContainer) {
resizeObserver.observe(observedContainer);
}

return () => {
if (observedContainer) {
resizeObserver.unobserve(observedContainer);
}
};
}, []);

// add a resizeobserver to the div that fires when it gets resized
// this is a workaround for the vega-lite width property not updating when the container size changes

return executionData.length > 1 ? (
<div
ref={containerRef}
style={{ width: '100%', paddingTop: '2px', borderTop: '1px solid var(--jp-toolbar-border-color)' }}
>
<Vega spec={getBarSpec(executionData)} actions={false} style={{ width: '100%' }} />
</div>
) : (
<> </>
);
}

const getBarSpec = executionData => {
// get date of first and last executionData
const firstDate = executionData[0].date;
const lastDate = executionData.at(-1).date;

// get the number of milliseconds between the first and last date
const dateDifference = lastDate.getTime() - firstDate.getTime();

// Set timeUnit according to time Range
// * less than 1 hour: 'minute'
// * less than 2 days: 'hour'
// * less than 2 months: yearmonthdate
// * else yearMonth
let timeUnit = 'yearMonth';
if (dateDifference < 60 * 60 * 1000) {
timeUnit = 'hoursminutesseconds';
} else if (dateDifference < 2 * 24 * 60 * 60 * 1000) {
timeUnit = 'dayhours';
} else if (dateDifference < 2 * 30 * 24 * 60 * 60 * 1000) {
timeUnit = 'monthdate';

Check warning on line 70 in src/Overview/TimeAxis.tsx

View workflow job for this annotation

GitHub Actions / build

'timeUnit' is assigned a value but never used
}

const spec = {
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
description: 'A simple bar chart with embedded data.',
data: {
values: executionData
},
width: 'container',
height: 25,
padding: { bottom: 0, top: 0, left: 5, right: 5 },
view: { stroke: null },
encoding: {
x: {
field: 'date',
title: '',
type: 'temporal',
axis: {
grid: false,
labelFlush: true,
labelAngle: 0,
tickBand: 'center',
tickExtra: true,
labelOverlap: 'greedy'
// labelExpr: "timeFormat(datum.value, '%b %d')"
// values: [ // TODO set values according to where labels should be placed
// 1696578026134, 1697534180510, 1697436168008, 1698746134246, 1699270720827, 1699517443585, 1701070650281
// ]
},
scale: { nice: false }
// timeUnit
},
y: {
field: 'cellExecutions',
title: null,
type: 'quantitative',
axis: false, //{ grid: false },
scale: { type: 'linear', nice: false },
stack: 'zero',
aggregate: 'sum'
}
},
layer: [
{
// TODO does not work in all cases
transform: [{ filter: 'datum.isVisible' }, { extent: 'date', param: 'date_extent' }],
mark: {
type: 'rect',
opacity: 0.6,
color: '#dedede'
},
encoding: {
x: { value: { expr: "scale('x', date_extent[0])-5" } },
x2: { value: { expr: "scale('x', date_extent[1])+5" } },
y2: { value: 25 },
y: { value: -5 }
}
},
{
mark: { type: 'bar', opacity: 1, color: '#bbb', tooltip: true },
encoding: {}
},
{
transform: [{ filter: 'datum.isVisible' }],
mark: {
type: 'bar',
opacity: 1,
color: '#333',
tooltip: true
},
encoding: {}
}
]
} as VisualizationSpec;
console.log('spec', spec);
return spec;
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LoopsSidebar } from './Overview/LoopsSidebar';
import { FileManager } from './Provenance/FileManager';
import { NotebookTrrack } from './Provenance/NotebookTrrack';
import { loopsLabIcon } from './loopsLabIcon';
import { useLoopsStore } from './LoopsStore';

// Storage of notebooks and their trrack provenance
export const notebookModelCache = new Map<Notebook, NotebookTrrack>();
Expand Down Expand Up @@ -45,6 +46,7 @@ function activate(
// called when the current notebook changes
// only tracks notebooks! not other files or tabs
console.info('notebook changed. New Notebook:', notebookEditor?.title.label);
useLoopsStore.getState().clearStateData(); // clear the state data when the notebook changes
if (notebookEditor) {
//testEventHandlers(notebookEditor);
notebookEditor.sessionContext.ready.then(() => {
Expand Down
45 changes: 45 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,48 @@ export const getScrollParent = (node: Element): Element => {
}
return document.scrollingElement || document.documentElement;
};

export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

export function throttle(func: (...args: any[]) => void, limit: number) {
let inThrottle;
return function (...args: any[]) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}

export function throttleAndDebounce(func, throttleTime, debounceTime) {
let timeout;
let lastExec = 0;

return function wrapper(...args) {
const elapsed = Date.now() - lastExec;

const later = () => {
lastExec = Date.now();
func(...args);
};

clearTimeout(timeout);

if (elapsed > throttleTime) {
later();
} else {
timeout = setTimeout(later, debounceTime);
}
};
}
Loading

0 comments on commit e87c399

Please sign in to comment.