Skip to content

Commit

Permalink
Merge pull request #476 from multinet-app/smaller-rows-cells
Browse files Browse the repository at this point in the history
Allow setting cell size smaller and fix lineup infinite re-render bug and sort logic
  • Loading branch information
JackWilb committed Aug 28, 2023
2 parents ac26042 + 59e1354 commit 8ee7623
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module.exports = {
'no-debugger': ['error'],
'vue/max-len': ['off'],
'import/prefer-default-export': ['off'],
'no-underscore-dangle': ['error', { allow: ['_id', '_from', '_to', '_key', '_type'] }],
'no-underscore-dangle': ['error', { allow: ['_id', '_from', '_to', '_key', '_type', '_index'] }],
...a11yOff,
'no-param-reassign': ['error', { props: false }],
'import/extensions': ['error', { ts: 'never', vue: 'always' }],
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const { rightClickMenu, sortBy } = storeToRefs(store);
<v-list-item
v-if="rightClickMenu.nodeID !== undefined"
dense
@click="sortBy.node = sortBy.node === rightClickMenu.nodeID ? null : rightClickMenu.nodeID"
@click="sortBy = { ...sortBy, node: sortBy.node === rightClickMenu.nodeID ? null : rightClickMenu.nodeID }"
>
<v-list-item-content>
<v-list-item-title>{{ sortBy.node === rightClickMenu.nodeID ? 'Remove Neighbor Sort' : 'Sort Neighbors' }}</v-list-item-title>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ControlPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ watch(expandedPanels, () => {
clearable
outlined
dense
@change="sortBy.node = null"
@change="sortBy = { ...sortBy, node: (sortBy.network === null ? sortBy.node : null), lineup: null }"
/>
</v-list-item>

Expand Down Expand Up @@ -184,7 +184,7 @@ watch(expandedPanels, () => {
Cell Size
<v-slider
v-model="cellSize"
:min="10"
:min="5"
:max="100"
:label="String(cellSize)"
class="px-2 align-center"
Expand Down
101 changes: 78 additions & 23 deletions src/components/LineUp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LineUp, {
} from 'lineupjs';
import { isInternalField } from '@/lib/typeUtils';
import { storeToRefs } from 'pinia';
import { arraysAreEqual } from '@/lib/provenanceUtils';
const store = useStore();
const {
Expand All @@ -17,6 +18,7 @@ const {
cellSize,
sortOrder,
lineupIsNested,
sortBy,
} = storeToRefs(store);
const lineup = ref<LineUp | null>(null);
Expand All @@ -29,38 +31,68 @@ if (matrixElement !== null) {
matrixResizeObserver.observe(matrixElement);
}
const lineupOrder = computed(() => {
if (lineup.value === null || [...lineup.value.data.getFirstRanking().getOrder()].length === 0) {
return [...Array(network.value?.nodes.length).keys()];
const lineupOrder = ref<number[]>(Array.from({ length: network.value !== null ? network.value.nodes.length : 0 }, (_, i) => i));
function hideIndexColumn(lineupDiv: HTMLElement) {
const colNumber = [...lineupDiv.getElementsByClassName('lu-th-label')]
.filter((label) => (label as HTMLElement).innerText === '_index')[0]
.parentElement?.getAttribute('data-id');
const elements = document.querySelectorAll(`[data-id="${colNumber}"]:not(.hidden)`);
if (elements.length > 2) {
elements.forEach((element) => {
element.classList.add('hidden');
});
} else {
// Retry after 100ms
setTimeout(() => {
hideIndexColumn(lineupDiv);
}, 200);
}
return [...lineup.value.data.getFirstRanking().getOrder()];
});
}
// If store order has changed, update lineup
let permutingMatrix = structuredClone(sortOrder.value.row);
watch(sortOrder, (newSortOrder) => {
if (lineup.value !== null) {
permutingMatrix = structuredClone(newSortOrder.row);
lineup.value.data.getFirstRanking().setSortCriteria([]);
const sortedData = newSortOrder.row.map((i) => (network.value !== null ? network.value.nodes[i] : {}));
(lineup.value.data as LocalDataProvider).setData(sortedData);
if (
lineup.value !== null
&& newSortOrder.row.length === lineupOrder.value.length
&& !arraysAreEqual(newSortOrder.row, lineupOrder.value)
) {
// Update the index column
newSortOrder.row.forEach((sortIndex, i) => {
(lineup.value?.data as LocalDataProvider).data[sortIndex]._index = i;
});
// Sort the index column
const col = (lineup.value.data as LocalDataProvider).find((d) => d.desc.label === '_index');
col?.markDirty('values'); // tell lineup that
col?.sortByMe(true);
}
});
// If lineup order has changed, update matrix
watch(lineupOrder, () => {
if (lineup.value !== null && network.value !== null) {
lineup.value.data.getFirstRanking().setSortCriteria([]);
const sortedData = sortOrder.value.row.map((i) => (network.value !== null ? network.value.nodes[i] : {}));
(lineup.value.data as LocalDataProvider).setData(sortedData);
watch(lineupOrder, (newOrder) => {
hideIndexColumn(document.getElementById('lineup') as HTMLElement);
// If the order is empty, don't update
if (newOrder.length === 0) {
return;
}
// If the order is the same as the matrix, don't update
if (arraysAreEqual(newOrder, sortOrder.value.row)) {
sortBy.value.lineup = null;
return;
}
// Otherwise, update the matrix with the lineup sort order
sortBy.value = {
lineup: newOrder,
network: null,
node: null,
};
});
// Helper functions
function idsToIndices(ids: string[]) {
const sortedData = permutingMatrix.map((i) => (network.value !== null ? network.value.nodes[i] : null));
return ids.map((nodeID) => sortedData.findIndex((node) => (node === null ? false : node._id === nodeID)));
return ids.map((nodeID) => network.value.nodes.findIndex((node) => (node === null ? false : node._id === nodeID)));
}
// Update selection/hover from matrix
Expand All @@ -75,8 +107,7 @@ watchEffect(() => {
function indicesToIDs(indices: number[]) {
if (network.value !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return indices.map((index) => network.value!.nodes[permutingMatrix[index]]._id);
return indices.map((index) => network.value?.nodes[index]._id);
}
return [];
}
Expand All @@ -96,9 +127,13 @@ function buildLineup() {
const lineupDiv = document.getElementById('lineup');
if (network.value !== null && lineupDiv !== null) {
const columns = [...new Set(network.value.nodes.map((node) => Object.keys(node)).flat())].filter((column) => !isInternalField(column));
// Clone the data to avoid modifying the store and add an index column
const childrenKeys = [...new Set(network.value.nodes.map((node) => (node.children ? Object.keys(node.children[0]) : [])).flat())];
const childrenKeysObject = Object.fromEntries(childrenKeys.map((key) => [key, null]));
const lineupData = structuredClone(network.value.nodes).map((node, i) => ({ ...node, ...(node.children ? childrenKeysObject : {}), _index: i }));
const columns = [...new Set(lineupData.map((node) => Object.keys(node)).flat())].filter((column) => !isInternalField(column));
builder.value = new DataBuilder(network.value.nodes);
builder.value = new DataBuilder(lineupData);
// Config adjustments
builder.value
Expand All @@ -117,6 +152,9 @@ function buildLineup() {
.defaultRanking()
.build(lineupDiv);
// Hide the index column
hideIndexColumn(lineupDiv);
let lastHovered = '';
// Add an event watcher to update highlighted nodes
Expand All @@ -137,6 +175,13 @@ function buildLineup() {
[lastHovered] = hoveredIDs;
});
lineup.value?.data.on('orderChanged', (oldOrder, newOrder, c, d, reasonArray) => {
if (reasonArray[0] === 'group_changed') {
return;
}
lineupOrder.value = newOrder;
});
lineup.value.data.getFirstRanking().on('groupsChanged', (oldSortOrder: number[], newSortOrder: number[], oldGroups: { name: string }[], newGroups: { name: string }[]) => {
if (JSON.stringify(oldGroups.map((group) => group.name)) !== JSON.stringify(newGroups.map((group) => group.name))) {
if (lineup.value !== null && lineup.value.data.getFirstRanking().getGroupCriteria().length > 0) {
Expand Down Expand Up @@ -172,6 +217,8 @@ watch(cellSize, () => {
function removeHighlight() {
hoveredNodes.value = [];
}
const labelFontSize = computed(() => `${0.8 * cellSize.value}px`);
</script>

<template>
Expand All @@ -198,4 +245,12 @@ function removeHighlight() {
.le-body {
overflow: hidden !important;
}
.le-td {
font-size: v-bind(labelFontSize);
}
.hidden {
display: none !important;
}
</style>
2 changes: 1 addition & 1 deletion src/lib/provenanceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function isArray(input: unknown): input is Array<string> {
return typeof input === 'object' && input !== null && input.constructor === Array;
}

function arraysAreEqual<T>(a: T[], b: T[]) {
export function arraysAreEqual<T>(a: T[], b: T[]) {
return a.length === b.length && a.every((element, index) => element === b[index]);
}

Expand Down
10 changes: 5 additions & 5 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ export const useStore = defineStore('store', () => {
children: value.map((node) => structuredClone(node)),
_type: 'supernode',
neighbors: [] as string[],
degreeCount: 0,
[aggregatedBy.value!]: key,
}),
);
Expand Down Expand Up @@ -197,7 +196,6 @@ export const useStore = defineStore('store', () => {
const filteredNode: Node = {
_type: 'supernode',
neighbors: [],
degreeCount: 0,
_key: 'filtered',
_id: 'filtered',
_rev: '',
Expand All @@ -222,7 +220,7 @@ export const useStore = defineStore('store', () => {
}

// Reset sort order now that network has changed
sortBy.value = { network: null, node: null };
sortBy.value = { network: null, node: null, lineup: null };

// Recalculate neighbors
defineNeighbors(networkAfterOperations.nodes, networkAfterOperations.edges);
Expand Down Expand Up @@ -277,7 +275,8 @@ export const useStore = defineStore('store', () => {

// If there are label candidates, set the label variable to the first one
if (labelCandidate.length > 0) {
[labelVariable.value] = labelCandidate;
// eslint-disable-next-line prefer-destructuring
labelVariable.value = labelCandidate[0];
}
});

Expand Down Expand Up @@ -528,7 +527,8 @@ export const useStore = defineStore('store', () => {
return order;
}
const sortOrder = computed(() => {
const colOrder = sortBy.value.network === null ? range(network.value.nodes.length) : computeSortOrder(sortBy.value.network);
const colOrder = sortBy.value.lineup
|| (sortBy.value.network === null ? range(network.value.nodes.length) : computeSortOrder(sortBy.value.network));
const rowOrder = sortBy.value.node === null ? colOrder : computeSortOrder(sortBy.value.node, colOrder);

return {
Expand Down
4 changes: 2 additions & 2 deletions src/store/provenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useProvenanceStore = defineStore('provenance', () => {
const selectedNodes = ref<string[]>([]);
const selectedCell = ref<Cell | null>(null);
const aggregatedBy = ref<string | null>(null);
const labelVariable = ref<string | undefined>(undefined);
const labelVariable = ref<string | null>(null);
const expandedNodeIDs = ref<string[]>([]);
const degreeRange = ref<[number, number]>([0, 0]);
const slicingConfig = ref<SlicingConfig>({
Expand All @@ -27,7 +27,7 @@ export const useProvenanceStore = defineStore('provenance', () => {
isValidRange: false,
});
const sliceIndex = ref(0);
const sortBy = ref<{ network : string | null; node: string | null }>({ network: null, node: null });
const sortBy = ref<{ network : string | null; node: string | null; lineup: number[] | null }>({ network: null, node: null, lineup: null });

// A live computed state so that we can edit the values when trrack does undo/redo
const currentPiniaState = computed(() => ({
Expand Down
1 change: 0 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export interface Dimensions {

export interface Node extends TableRow {
neighbors: string[];
degreeCount: number;
children?: Node[];
parentPosition?: number;
_type?: string;
Expand Down

0 comments on commit 8ee7623

Please sign in to comment.