diff --git a/.gitignore b/.gitignore index 7749c1bd20bd40fb0e1d5746687a0dd15eabc63e..6411dbf0d520a9f0a126616ca9a00a2b86418394 100644 --- a/.gitignore +++ b/.gitignore @@ -80,7 +80,6 @@ node_modules tsconfig.tsbuildinfo vite.config.ts.* - # Generated ts definitions files for CSS modules *.d.ts *.module.scss.d.ts diff --git a/libs/shared/lib/vis/visualizations/matrix/components/BitmapTextLabel.ts b/libs/shared/lib/vis/visualizations/matrix/components/BitmapTextLabel.ts new file mode 100644 index 0000000000000000000000000000000000000000..4cc2a1d44d034bfc047fe099d4e35a1a448deee5 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/matrix/components/BitmapTextLabel.ts @@ -0,0 +1,20 @@ +import { BitmapText, Text } from 'pixi.js'; + +export class BitmapTextLabel extends BitmapText { + static readonly LABEL_FONT_FAMILY = 'HelveticaRegular'; + static readonly LABEL_FONT_SIZE = 12; + static readonly LABEL_COLOR = 0x333333; + + constructor(label: string) { + super(label, { + fontName: BitmapTextLabel.LABEL_FONT_FAMILY, + fontSize: BitmapTextLabel.LABEL_FONT_SIZE, + align: 'right', + tint: BitmapTextLabel.LABEL_COLOR, + }); + + this.rotation = (Math.PI * 3) / 2; + this.eventMode = 'none'; + this.name = 'Text_' + label; + } +} diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx index 7226749f9bf6d3e1f53aee905d84226ac19adef5..facb7ff10e4b7f6f4d5ffb2c5dede333dfd32c1a 100644 --- a/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx +++ b/libs/shared/lib/vis/visualizations/matrix/components/ColumnGraphicsComponent.tsx @@ -50,9 +50,14 @@ export const createColumn = ( gfx.drawRect(0, i * cellWidth, cellWidth, cellHeight); gfx.endFill(); } else { + // console.log('inOutEdge', inOutEdge, Math.min(inOutEdge.length, 1), currentVisMapping.colorScale(Math.min(inOutEdge.length, 1))); + if (outEdges.length > 0) { const thisEdge = outEdges[0]; + // console.log('inOutEdge', thisEdge); + const value = byString(thisEdge, currentVisMapping.attribute); + // console.log('value', value); color = currentVisMapping.colorScale(value); } @@ -62,8 +67,25 @@ export const createColumn = ( gfx.drawRect(0, i * cellWidth, cellWidth, cellHeight); gfx.endFill(); } + + // if (inOutEdge.length > 0) { + // color = fgCellColor; + // } else { + // color = bgCellColor; + // } } + // gfx.on('pointerdown', onButtonDown); + // .on('pointerup', onDragEnd).on('pointerupoutside', onDragEnd); + + // for (let i = 0; i < edges.length; i++) { + // const edge = edges[i]; + // // draw an rectangle with a fill + // gfx.beginFill(0xff0000, 0.5); + // gfx.drawRect(0, i * cellWidth, cellWidth, cellHeight * nodes.length); + // gfx.endFill(); + // } + return gfx; }; diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ColumnLabelTrack.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ColumnLabelTrack.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c0c00410da6e6b745e90edf4768f2803f92e2ca4 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/matrix/components/ColumnLabelTrack.tsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; + +export const ColumnLabelTrack = ({ columnLabels, config }: { columnLabels: string[]; config: any }) => { + // console.log('ColumnLabelTrack', columnLabels); + + return ( + <> + <div className="grid grid-flow-col auto-cols-max h-80"> + {columnLabels.map((label, index) => { + // console.log('columnlabeltrack', label); + return ( + <div className="-rotate-90 font-mono text-sm w-4" key={index}> + {label} + </div> + ); + })} + </div> + </> + ); +}; diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx index 6176f1d5b9e6859980f94dfbcaa4d49735338cfa..b04470376494f3a323e6c833ef6927c88773280a 100644 --- a/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx +++ b/libs/shared/lib/vis/visualizations/matrix/components/ColumnSpriteComponent.tsx @@ -11,6 +11,7 @@ export const createColumn = ( cellWidth: number, cellHeight: number ) => { + // console.log('createColumn', id, nodes.length, edges.length); const sprite = new Sprite(Texture.WHITE); sprite.name = 'col_' + id; sprite.eventMode = 'static'; @@ -19,6 +20,8 @@ export const createColumn = ( const edgesForThisColumn = edges.filter((edge) => { return edge.from === nodesCol[id].id || edge.to === nodesCol[id].id; }); + // console.log('edgesForThisColumn', nodesCol[id].id, edgesForThisColumn); + // const color = new Color({ r: 255 * (id / nodesCol.length), g: 255, b: 255, a: 1 }); const bgCellColor = dataColors.neutral['5']; const fgCellColor = tailwindColors.entity.DEFAULT; let color = bgCellColor; @@ -44,5 +47,14 @@ export const createColumn = ( // sprite.drawRect(0, i * cellWidth, cellWidth, cellHeight); // sprite.endFill(); } + // sprite.on('pointerdown', onButtonDown); + // .on('pointerup', onDragEnd).on('pointerupoutside', onDragEnd); + // for (let i = 0; i < edges.length; i++) { + // const edge = edges[i]; + // // draw an rectangle with a fill + // gfx.beginFill(0xff0000, 0.5); + // gfx.drawRect(0, i * cellWidth, cellWidth, cellHeight * nodes.length); + // gfx.endFill(); + // } return sprite; }; diff --git a/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx index 550a1cef542eae22dda7e79b345a47106cfbe80a..772898d3c0d12499e28bf878ac7f31d75971f194 100644 --- a/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx +++ b/libs/shared/lib/vis/visualizations/matrix/components/MatrixPixi.tsx @@ -1,4 +1,5 @@ import { Edge, GraphQueryResult, Node, useML, useSearchResultData } from '@graphpolaris/shared/lib/data-access'; +import { Cull } from '@pixi-essentials/cull'; import { dataColors, tailwindColors } from 'config'; import * as d3 from 'd3'; import { Viewport } from 'pixi-viewport'; @@ -25,6 +26,7 @@ import Color from 'color'; import { localConfigSchemaType } from '../../../Types'; import { createColumn } from './ColumnGraphicsComponent'; import { ReorderingManager } from './ReorderingManager'; +import { TextLabel } from './TextLabel'; type Props = { // onClick: (node: NodeType, pos: IPointData) => void; @@ -38,30 +40,41 @@ type Props = { }; const app = new Application({ background: 0xffffff, antialias: true, autoDensity: true, eventMode: 'auto' }); -const columnsContainer = new Container(); -const columnAxisContainer = new Container(); -const rowsContainer = new Container(); - ////////////////// // MAIN COMPONENT ////////////////// - export const MatrixPixi = (props: Props) => { + const columnLayer = new Container(); + const columnLabelLayer = new Container(); + const rowsLabelLayer = new Container(); + + let lastZoom = Infinity; + let config = { textOffsetX: 100, textOffsetY: 100, visMapping: [] as any[], // TODO type - cellHeight: 100, - cellWidth: 100, + CELL_HEIGHT: 50, + CELL_WIDTH: 50, + + LABEL_FONT_FAMILY: 'Roboto', + LABEL_FILL: 0x000000, + LABEL_ALIGN: 'center', + LABEL_FONT_SIZE: 25, + LABEL_MARGIN: 4, + LABEL_MIN_WIDTH: 6, + + ANIMATION_DURATION: 0.8, }; let columnOrder: string[] = []; let rowOrder: string[] = []; + const [columnOrderState, setColumnOrderState] = useState<string[]>([]); + const [rowOrderState, setRowOrderState] = useState<string[]>([]); const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>(); const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]); - // const [columnOrder, setColumnOrder] = useState<string[]>([]); const nodeMap = useRef(new Map<string, Graphics>()); const linkMap = useRef(new Map<string, Graphics>()); @@ -71,7 +84,8 @@ export const MatrixPixi = (props: Props) => { const ml = useML(); const searchResults = useSearchResultData(); - // const imperative = useRef<any>(null); + const cull = new Cull(); + let cullDirty = useRef(true); function resize() { const width = ref?.current?.clientWidth || 1000; @@ -107,8 +121,11 @@ export const MatrixPixi = (props: Props) => { } }, [ref]); + // useEffect(() => { + // console.log('newColumnOrder', columnOrder); + // }, [columnOrder]); + useEffect(() => { - console.log('graph change'); // console.log('graph changed', props.graph, ref.current, ref.current.children.length > 0, imperative.current); if (props.graph && ref.current && ref.current.children.length > 0) { if (!isSetup.current) setup(); @@ -117,7 +134,6 @@ export const MatrixPixi = (props: Props) => { }, [props.graph]); useEffect(() => { - console.log('config change'); // console.log('graph changed', props.graph, ref.current, ref.current.children.length > 0, imperative.current); if (props.graph && ref.current && ref.current.children.length > 0) { setup(); @@ -158,45 +174,45 @@ export const MatrixPixi = (props: Props) => { if (forceClear) { nodeMap.current.clear(); linkMap.current.clear(); - rowsContainer.removeChildren(); - columnsContainer.removeChildren(); + rowsLabelLayer.removeChildren(); + columnLayer.removeChildren(); } } }; const onHoverColumn = (event: any, currentNode: Node | null) => { - app.renderer.plugins.interaction.cursorStyles.pointer = 'crosshair'; + app.renderer.events.cursorStyles.pointer = 'crosshair'; if (!currentNode) { - rowsContainer.children.forEach((row) => { + rowsLabelLayer.children.forEach((row) => { row.alpha = 1; }); - columnsContainer.children.forEach((col) => { + columnLayer.children.forEach((col) => { col.alpha = 1; }); - columnAxisContainer.children.forEach((colText) => { + columnLabelLayer.children.forEach((colText) => { colText.alpha = 1; }); return; } - columnsContainer.children.forEach((col) => { + columnLayer.children.forEach((col) => { col.alpha = 0.25; }); event.currentTarget.alpha = 1; - columnAxisContainer.children.forEach((colText) => { + columnLabelLayer.children.forEach((colText) => { colText.alpha = 0.25; if (colText.name?.endsWith(currentNode.id)) colText.alpha = 1; }); if (!props.graph) return; - rowsContainer.children.forEach((col) => { + rowsLabelLayer.children.forEach((col) => { col.alpha = 0.25; }); if (!props.graph) return; - rowsContainer.children.forEach((col) => { + rowsLabelLayer.children.forEach((col) => { col.alpha = 0.25; }); @@ -206,17 +222,17 @@ export const MatrixPixi = (props: Props) => { }); edgesForThisColumn.forEach((edge) => { - let row = rowsContainer.getChildByName(edge.to); + let row = rowsLabelLayer.getChildByName(edge.to); if (row) row.alpha = 1; - row = rowsContainer.getChildByName(edge.from); + row = rowsLabelLayer.getChildByName(edge.from); if (row) row.alpha = 1; }); }; const reorderColumns = (columnOrder: string[]) => { - const columns = columnsContainer.children; - const columnsText = columnAxisContainer.children; + const columns = columnLayer.children; + const columnLabels = columnLabelLayer.children; let columnPositions: Point[] = []; for (let i = 0; i < columns.length; i++) { const col = columns[i]; @@ -224,8 +240,8 @@ export const MatrixPixi = (props: Props) => { } let columnTextPositions: Point[] = []; - for (let i = 0; i < columnsText.length; i++) { - const col = columnsText[i]; + for (let i = 0; i < columnLabels.length; i++) { + const col = columnLabels[i]; columnTextPositions.push(new Point(col.position.x, col.position.y)); } @@ -246,7 +262,7 @@ export const MatrixPixi = (props: Props) => { const newPositionText = columnTextPositions[index]; // move column to new position - Actions.moveTo(columnsText[i], newPositionText.x, newPositionText.y, 1, Interpolations.pow2out).play(); + Actions.moveTo(columnLabels[i], newPositionText.x, newPositionText.y, 1, Interpolations.pow2out).play(); } }; @@ -259,7 +275,7 @@ export const MatrixPixi = (props: Props) => { cellWidth: number, cellHeight: number ) => { - const rows = rowsContainer.children; + const rows = rowsLabelLayer.children; let rowPositions: Point[] = []; for (let i = 0; i < rows.length; i++) { const row = rows[i]; @@ -277,34 +293,35 @@ export const MatrixPixi = (props: Props) => { Actions.moveTo(row, newPosition.x, newPosition.y, 1, Interpolations.pow2out).play(); } - const colOrder = columnsContainer.children.map((col) => col.name) as string[]; + const colOrder = columnLayer.children.map((col) => col.name) as string[]; console.log('colOrder', colOrder); if (!colOrder) throw new Error('colOrder is undefined'); - columnsContainer.removeChildren(); + columnLayer.removeChildren(); setupColumns(edges, colOrder, rowOrder); }; const setup = () => { if (!props.graph) throw Error('Graph is undefined; setup not possible'); - columnsContainer.removeChildren(); - rowsContainer.removeChildren(); - columnAxisContainer.removeChildren(); + console.log('setup matrixvis with graph:', props.graph); + + columnLayer.removeChildren(); + rowsLabelLayer.removeChildren(); + columnLabelLayer.removeChildren(); + viewport.current?.removeAllListeners(); + viewport.current?.removeChildren(); app.stage.removeChildren(); const size = ref.current?.getBoundingClientRect(); viewport.current = new Viewport({ screenWidth: size?.width || 1000, screenHeight: size?.height || 1000, - worldWidth: size?.width || 1000, - worldHeight: size?.height || 1000, stopPropagation: true, events: app.renderer.events, // the interaction module is important for wheel to work properly when renderer.view is placed or scaled }); - app.stage.addChild(viewport.current); + // setupZoomLogic(); - viewport.current.addChild(columnsContainer); - viewport.current.addChild(rowsContainer); + viewport.current.addChild(columnLayer); const groupByType = props.graph.nodes.reduce((group: any, node: Node) => { if (!group[node.label]) group[node.label] = []; @@ -345,30 +362,126 @@ export const MatrixPixi = (props: Props) => { columnOrder = newOrdering.columnOrder; rowOrder = newOrdering.rowOrder; - config.cellWidth = Math.max((size?.width || 1000) / props.graph.nodes.length, (size?.height || 1000) / props.graph.nodes.length); - config.cellHeight = config.cellWidth; - - // console.log('currentColumnOrder', columnOrder); - setupVisualizationEncodingMapping(props.localConfig); setupColumns(props.graph.edges, columnOrder, rowOrder); - setupColumnLegend(columnOrder); + setupColumnLabels(columnOrder); setupColumnInteractivity(cols); - setupRowLegend(rows, rowOrder); + setupRowLabels(rows, rowOrder); setupRowInteractivity(cols, rows, rowOrder, d3.scaleOrdinal(d3.schemeCategory10)); - console.log('setup matrixvis with graph:', props.graph); - // activate plugins viewport.current.drag().pinch().wheel().animate({}).decelerate({ friction: 0.75 }); + lastZoom = viewport.current.scale.x; + app.stage.addChild(viewport.current); app.ticker.add(tick); + + // requestRender(); update(); isSetup.current = true; + + cull.clear(); + cull.addAll(viewport.current.children); + cull.addAll(rowsLabelLayer.children); + cull.addAll(columnLabelLayer.children); + cull.addAll(columnLayer.children); + cullDirty.current = true; + + setColumnOrderState([...columnOrder]); + setRowOrderState([...rowOrder]); + + resetViewport(); }; + // let renderRequestId: number | undefined = undefined; + // const requestRender = () => { + // if (renderRequestId) { + // return; + // } + // renderRequestId = window.requestAnimationFrame(() => { + // app.render(); + // renderRequestId = undefined; + // }); + // }; + + const updateVisibility = () => { + if (!viewport.current) return; + + // culling + if (viewport.current.dirty || cullDirty.current) { + cull.cull(app.renderer.screen); + // console.log( + // 'culling', + // [...viewport.current.children].filter((x) => x.visible === true).length, + // [...viewport.current.children].filter((x) => x.visible === false).length, + // [...rowsLabelLayer.children].filter((x) => x.visible === true).length, + // [...rowsLabelLayer.children].filter((x) => x.visible === false).length, + // [...columnLabelLayer.children].filter((x) => x.visible === true).length, + // [...columnLabelLayer.children].filter((x) => x.visible === false).length, + // [...columnLayer.children].filter((x) => x.visible === true).length, + // [...columnLayer.children].filter((x) => x.visible === false).length + // ); + + viewport.current.dirty = false; + cullDirty.current = false; + } + + // levels of detail + // if (viewport.current?.scale.x !== lastZoom) { + const zoom = viewport.current?.scale.x || 0.0; + + const oneColumn = columnLayer.children[0]; + if (!oneColumn) return; + + const targetLabelWidth = oneColumn.getBounds().width; + const targetLabelHeight = targetLabelWidth; //cells have same width and height + + [...columnLabelLayer.children] + .filter((x) => x.visible === true) + .forEach((columnItem) => { + if (targetLabelWidth < config.LABEL_MIN_WIDTH) { + columnItem.visible = false; + } else { + const textLabel = columnItem as TextLabel; + textLabel.visible = true; + } + }); + + [...rowsLabelLayer.children] + .filter((x) => x.visible === true) + .forEach((rowItem) => { + if (targetLabelWidth < config.LABEL_MIN_WIDTH) { + rowItem.visible = false; + } else { + rowItem.visible = true; + } + }); + + lastZoom = zoom; + // } + }; + + // TODO + // const setupZoomLogic = () => { + // const zoomIn = () => { + // if (!viewport.current) return + // viewport.current.zoom(-WORLD_WIDTH / 10, true); + // }; + // const zoomOut = () => { + // if (!viewport.current) return; + // viewport.current.zoom(WORLD_WIDTH / 10, true); + // }; + const resetViewport = () => { + if (!viewport.current) return; + viewport.current.center = new Point(viewport.current.worldWidth / 2, viewport.current.worldHeight / 2); + + // console.log('resetViewPort', viewport.current.center); + viewport.current.fitWorld(true); + }; + // } + const setupVisualizationEncodingMapping = (localConfig: localConfigSchemaType) => { if (!props.graph) throw new Error('Graph is undefined; cannot setup matrix'); const visMapping = []; // TODO type @@ -391,10 +504,10 @@ export const MatrixPixi = (props: Props) => { gfxContext.lineStyle(2, 0xffffff); if (this.encoding === 'rect') { - gfxContext.drawRect(0, i * config.cellHeight, config.cellWidth, config.cellHeight); + gfxContext.drawRect(0, i * config.CELL_HEIGHT, config.CELL_WIDTH, config.CELL_HEIGHT); } if (this.encoding === 'circle') { - gfxContext.drawCircle(config.cellWidth / 2, i * config.cellHeight + config.cellHeight / 2, config.cellWidth / 2); + gfxContext.drawCircle(config.CELL_WIDTH / 2, i * config.CELL_HEIGHT + config.CELL_HEIGHT / 2, config.CELL_WIDTH / 2); } gfxContext.endFill(); @@ -442,20 +555,28 @@ export const MatrixPixi = (props: Props) => { }); // console.log('edgesForThisColumn', oneColumn.name, edgesForThisColumn); - const col = createColumn(rowOrder, edgesForThisColumn, config.visMapping, config.cellWidth, config.cellHeight); + const col = createColumn(rowOrder, edgesForThisColumn, config.visMapping, config.CELL_WIDTH, config.CELL_HEIGHT); oneColumn.addChild(col); - oneColumn.position.set(j * config.cellWidth + config.textOffsetX, config.textOffsetY); - - columnsContainer.addChild(oneColumn); - oneColumn.alpha = 0; - Actions.sequence(Actions.delay(j * 0.005 * Math.random()), Actions.fadeIn(oneColumn, 0.8, Interpolations.pow2out)).play(); + oneColumn.position.set(j * config.CELL_WIDTH + config.textOffsetX, config.textOffsetY); + + columnLayer.addChild(oneColumn); + + if (columnOrder.length < 100) { + oneColumn.alpha = 0; + Actions.sequence( + Actions.delay(j * 0.005 * Math.random()), + Actions.fadeIn(oneColumn, config.ANIMATION_DURATION, Interpolations.pow2out) + ).play(); + } else { + oneColumn.alpha = 1; + } } } const setupColumnInteractivity = (cols: Node[]) => { - for (let j = 0; j < columnsContainer.children.length; j++) { - const oneColumn = columnsContainer.children[j]; - oneColumn.interactive = true; + for (let j = 0; j < columnLayer.children.length; j++) { + const oneColumn = columnLayer.children[j]; + oneColumn.eventMode = 'dynamic'; oneColumn.on('pointerdown', onButtonDown); oneColumn.on('pointerover', (event) => { const col = cols.find((col) => col.id === oneColumn.name); @@ -467,34 +588,28 @@ export const MatrixPixi = (props: Props) => { } }; - const setupColumnLegend = (columnOrder: string[]) => { - // console.log('setupColumnLegend'); - // columnAxisContainer.removeChildren(); - columnAxisContainer.position.set(config.textOffsetX, 0); + const setupColumnLabels = (columnOrder: string[]) => { + columnLabelLayer.removeChildren(); + + columnLabelLayer.position.set(config.textOffsetX, 0); for (let j = 0; j < columnOrder.length; j++) { - // from the PixiJS documention: Setting a text object's scale to > 1.0 will result in blurry/pixely display, - // because the text is not re-rendered at the higher resolution needed to look sharp - - // it's still the same resolution it was when generated. To deal with this, you can render at a higher - // initial size and down-scale, instead. - const finalTextSize = 10; - const bigRandomScaleFactor = 10; - const basicText = new Text(columnOrder[j], { fontSize: finalTextSize * bigRandomScaleFactor, fill: 0x000000, align: 'center' }); - basicText.position.set(j * config.cellWidth + config.cellWidth / 4, config.textOffsetY - 5); - basicText.scale.set(finalTextSize / (bigRandomScaleFactor * bigRandomScaleFactor)); - basicText.rotation = (Math.PI * 3) / 2; - basicText.eventMode = 'none'; - basicText.name = 'Text_' + columnOrder[j]; - columnAxisContainer.addChild(basicText); + const textLabel = new TextLabel(columnOrder[j], config); + // const textLabel = new BitmapTextLabel(columnOrder[j]); + + // console.log('font resolved', columnOrder[j]); + textLabel.position.set(j * config.CELL_WIDTH + config.CELL_WIDTH / 4, config.textOffsetY - 5); + columnLabelLayer.addChild(textLabel); } - viewport.current?.addChild(columnAxisContainer); + viewport.current?.addChild(columnLabelLayer); const columnAxis = new Sprite(Texture.WHITE); - columnAxis.width = columnOrder.length * config.cellWidth; + columnAxis.width = columnOrder.length * config.CELL_WIDTH; columnAxis.height = config.textOffsetY; columnAxis.tint = 0xaaaaaa; columnAxis.position.set(config.textOffsetX, 0); columnAxis.alpha = 0.0; - columnAxis.interactive = true; + // columnAxis.interactive = true; + columnAxis.eventMode = 'dynamic'; columnAxis.on('pointerdown', (event) => { // const reordering = new ReorderColumnsAction(columnOrder); @@ -508,10 +623,10 @@ export const MatrixPixi = (props: Props) => { }); // columnAxis.on('pointerdown', reorderColumns); columnAxis.on('pointerover', (event) => { - Actions.fadeTo(columnAxis, 0.5, 0.5, Interpolations.pow2out).play(); + Actions.fadeTo(columnAxis, 0.5, config.ANIMATION_DURATION, Interpolations.pow2out).play(); }); columnAxis.on('pointerout', (event) => { - Actions.fadeTo(columnAxis, 0.0, 0.5, Interpolations.pow2out).play(); + Actions.fadeTo(columnAxis, 0.0, config.ANIMATION_DURATION, Interpolations.pow2out).play(); }); viewport.current?.addChild(columnAxis); }; @@ -535,35 +650,46 @@ export const MatrixPixi = (props: Props) => { return array; } - function setupRowLegend(rows: Node[], rowOrder: string[]) { + function setupRowLabels(rows: Node[], rowOrder: string[]) { + rowsLabelLayer.removeChildren(); + + // columnLabelLayer.position.set(config.textOffsetX, 0); for (let i = 0; i < rowOrder.length; i++) { const rowID = rowOrder[i]; - const finalTextSize = 10; - const bigRandomScaleFactor = 10; - const basicText = new Text(rowID, { fontSize: finalTextSize * bigRandomScaleFactor, fill: 0x000000, align: 'right' }); - basicText.anchor.x = 1; - basicText.anchor.y = 0; - basicText.scale.set(finalTextSize / (bigRandomScaleFactor * bigRandomScaleFactor)); - basicText.position.set(config.textOffsetX - 5, i * config.cellHeight + config.textOffsetY + config.cellHeight / 4); - basicText.name = rowID; - rowsContainer.addChild(basicText); - - Actions.sequence(Actions.delay(i * 0.005 * Math.random()), Actions.fadeIn(basicText, 0.5, Interpolations.pow2out)).play(); + const textLabel = new TextLabel(rowID, config); + textLabel.anchor.x = 1; + textLabel.anchor.y = 0; + textLabel.rotation = 0; + textLabel.name = rowID; + textLabel.position.set(config.textOffsetX - 5, i * config.CELL_HEIGHT + config.textOffsetY + config.CELL_HEIGHT / 4); + rowsLabelLayer.addChild(textLabel); + + if (rowOrder.length < 100) { + textLabel.alpha = 0; + Actions.sequence( + Actions.delay(i * 0.005 * Math.random()), + Actions.fadeIn(textLabel, config.ANIMATION_DURATION, Interpolations.pow2out) + ).play(); + } else { + textLabel.alpha = 1; + } } + + viewport.current?.addChild(rowsLabelLayer); } const setupRowInteractivity = (cols: Node[], rows: Node[], rowOrder: string[], colorScale: any) => { const rowAxis = new Sprite(Texture.WHITE); rowAxis.width = config.textOffsetX; - rowAxis.height = rows.length * config.cellHeight; + rowAxis.height = rows.length * config.CELL_HEIGHT; rowAxis.tint = 0xaaaaaa; rowAxis.position.set(0, config.textOffsetY); rowAxis.alpha = 0.0; - rowAxis.interactive = true; + rowAxis.eventMode = 'dynamic'; rowAxis.on('pointerdown', (event) => { if (props.graph) { rowOrder = shuffle(rowOrder); - reorderRows(cols, rows, props?.graph.edges, rowOrder, colorScale, config.cellWidth, config.cellHeight); + reorderRows(cols, rows, props?.graph.edges, rowOrder, colorScale, config.CELL_WIDTH, config.CELL_HEIGHT); } }); rowAxis.on('pointerover', (event) => { @@ -578,6 +704,8 @@ export const MatrixPixi = (props: Props) => { const tick = (delta: number) => { if (props.graph) { Actions.tick(delta / 60); + + updateVisibility(); } }; @@ -587,6 +715,8 @@ export const MatrixPixi = (props: Props) => { <NLPopup onClose={() => {}} data={popup} key={popup.node.id} /> ))} {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />} + + {/* {columnOrderState.length > 0 && <ColumnLabelTrack columnLabels={columnOrderState} config={config} />} */} <div className="h-full w-full overflow-hidden" ref={ref}></div> </> ); diff --git a/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx b/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx index ae69d44e285be9e15fd93bb3cdaf49e9dd4f5398..a843663d38acb3dd118833232ccbca5ddf40f6ea 100644 --- a/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx +++ b/libs/shared/lib/vis/visualizations/matrix/components/ReorderingManager.tsx @@ -10,6 +10,27 @@ export class ReorderingManager { this.graph = graph; } + // // Precompute the orders. + // orders = { + // // + // // name: d3.range(n).sort(function (a, b) { + // // return d3.ascending(nodes[a].name, nodes[b].name); + // // }), + // // count: d3.range(n).sort(function (a, b) { + // // return nodes[b].count - nodes[a].count; + // // }), + // // group: d3.range(n).sort(function (a, b) { + // // var x = nodes[b].group - nodes[a].group; + // // return x != 0 ? x : d3.ascending(nodes[a].name, nodes[b].name); + // // }), + // leafOrder: computeLeaforder, + // // leafOrderDist: computeLeaforderDist, + // // barycenter: computeBarycenter, + // // rcm: computeRCM, + // // spectral: computeSpectral, + // // nn2opt: computeNN2OPT, + // }; + private computeNameOrder = (columnOrder: string[], rowOrder: string[]) => { const columnOrderSorted = columnOrder.sort((a, b) => a.localeCompare(b)); const rowOrderSorted = rowOrder.sort((a, b) => a.localeCompare(b)); @@ -80,6 +101,8 @@ export class ReorderingManager { adjacency.push(rowOrderIDs); } + // console.log('adjacency', adjacency); + return adjacency; } @@ -114,6 +137,9 @@ export class ReorderingManager { ); const nodes = columnOrder.map((id) => nodesTemp.find((node) => node.id === id)); + const nodesWithRows = nodes.push(...rowOrder.map((id) => nodesTemp.find((node) => node.id === id))); + console.log('nodesTemp', nodes); + const graphReorderJs = reorderAny.graph().nodes(nodes); const edges = graph.edges.map((edge) => { @@ -130,6 +156,7 @@ export class ReorderingManager { } as any; }); + // const edges = rowOrder.map((id) => edgesTemp.find((edge) => edge.id === id)); graphReorderJs.links(edges); graphReorderJs.init(); @@ -154,6 +181,14 @@ export class ReorderingManager { const reorderGraph = this.getReorderJSGraph(this.graph, columnOrder, rowOrder); var order = reorderAny.spectral_order(reorderGraph); + // rcm.forEach(function (lo, i) { + // nodes[i].rcm = lo; + // }); + + // return nodes.map(function (n) { + // return n.rcm; + // }); + const columnOrderSorted = order.map((i: number) => columnOrder[i]).filter((a: string) => columnOrder.includes(a)); const rowOrderSorted = order.map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a)); @@ -167,6 +202,13 @@ export class ReorderingManager { const reorderGraph = this.getReorderJSGraph(this.graph, columnOrder, rowOrder); var order = reorderAny.bfs_order(reorderGraph); + // rcm.forEach(function (lo, i) { + // nodes[i].rcm = lo; + // }); + + // return nodes.map(function (n) { + // return n.rcm; + // }); const columnOrderSorted = order.map((i: number) => columnOrder[i]).filter((a: string) => columnOrder.includes(a)); const rowOrderSorted = order.map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a)); @@ -183,6 +225,7 @@ export class ReorderingManager { const order = reorderAny.pca_order(columnOrderUnSorted); const columnOrderSorted = order.map((i: number) => columnOrder[i]); + // const rowOrderSorted = order.map((i: number) => rowOrder[i]).filter((a: string) => rowOrder.includes(a)); console.debug('improved pca', order, columnOrderSorted, rowOrder); @@ -193,22 +236,22 @@ export class ReorderingManager { console.log('reorderMatrix', orderingname); switch (orderingname.toLowerCase()) { case 'leafordering': { - return this.computeLeaforder(columnOrder, rowOrder); + return this.computeLeaforder(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } case 'name': { - return this.computeNameOrder(columnOrder, rowOrder); + return this.computeNameOrder(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } case 'count': { - return this.computeCountOrder(columnOrder, rowOrder); + return this.computeCountOrder(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } case 'barycenter': { - return this.computeBarycenter(columnOrder, rowOrder); + return this.computeBarycenter(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } case 'rcm': { - return this.computeRCM(columnOrder, rowOrder); + return this.computeRCM(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } case 'spectral': { - return this.computeSpectal(columnOrder, rowOrder); + return this.computeSpectal(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } // case 'pca': { // return this.computePCA(columnOrder, rowOrder); @@ -217,10 +260,10 @@ export class ReorderingManager { // return this.computeBFS(columnOrder, rowOrder); // } case 'none' || 'identity': { - return { columnOrder, rowOrder }; + return { columnOrder, rowOrder } as { columnOrder: string[]; rowOrder: string[] }; } default: { - return this.computeLeaforder(columnOrder, rowOrder); + return this.computeLeaforder(columnOrder, rowOrder) as { columnOrder: string[]; rowOrder: string[] }; } } }; diff --git a/libs/shared/lib/vis/visualizations/matrix/components/TextLabel.ts b/libs/shared/lib/vis/visualizations/matrix/components/TextLabel.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e6e88d389502c074266a7a867c9156b2c3985a7 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/matrix/components/TextLabel.ts @@ -0,0 +1,34 @@ +import { Text } from 'pixi.js'; + +export class TextLabel extends Text { + static readonly RANDOM_SCALE_FACTOR = 10.0; + + constructor(label: string, config: any) { + super(label); + + // this.style = { fontSize: this.finalTextSize * this.bigRandomScaleFactor, fill: 0x000000, align: 'center' }; + this.style = { + fontSize: config.LABEL_FONT_SIZE * TextLabel.RANDOM_SCALE_FACTOR, + fontFamily: config.LABEL_FONT_FAMILY, + fill: config.LABEL_FILL, + align: config.LABEL_ALIGN, + }; + this.scale.x = 1 / TextLabel.RANDOM_SCALE_FACTOR; + this.scale.y = 1 / TextLabel.RANDOM_SCALE_FACTOR; + this.rotation = (Math.PI * 3) / 2; + this.eventMode = 'none'; + this.name = 'Text_' + label; + this.cacheAsBitmap = true; + + // // from the PixiJS documention: Setting a text object's scale to > 1.0 will result in blurry/pixely display, + // // because the text is not re-rendered at the higher resolution needed to look sharp - + // // it's still the same resolution it was when generated. To deal with this, you can render at a higher + // // initial size and down-scale, instead. + // const basicText = new Text(columnOrder[j], { fontSize: finalTextSize * bigRandomScaleFactor, fill: 0x000000, align: 'center' }); + // basicText.position.set(j * config.cellWidth + config.cellWidth / 4, config.textOffsetY - 5); + // basicText.scale.set(finalTextSize / (bigRandomScaleFactor * bigRandomScaleFactor)); + // basicText.rotation = (Math.PI * 3) / 2; + // basicText.eventMode = 'none'; + // basicText.name = 'Text_' + columnOrder[j]; + } +} diff --git a/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx b/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx index ea5977146249d3f88051f58c14083c0d51fcd192..766845b3ce986e7a81d00c0c8ccf456ba0e68842 100644 --- a/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx +++ b/libs/shared/lib/vis/visualizations/matrix/matrix.stories.tsx @@ -8,8 +8,19 @@ import { big2ndChamberSchemaRaw, smallFlightsQueryResults, mockLargeQueryResults, + recommendationPersonActedInMovieQueryResult, + recommendationPersonActedInMovieQueryResultPayload, + slackReactionToThreadedMessageQueryResultPayload, } from '../../../mock-data'; +// export * from './mockLargeQueryResults'; +// export * from './big2ndChamberQueryResult'; +// export * from './bigMockQueryResults'; +// export * from './mockQueryResults'; +// export * from './smallFlightsQueryResults'; +// export * from './mockMobilityQueryResult'; +// export * from './typesMockQueryResults'; + import { VisualizationPanel } from '../../panel/visualization'; import { assignNewGraphQueryResult, @@ -43,7 +54,7 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore: any = configureStore({ +const Mockstore = configureStore({ reducer: { schema: schemaSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, @@ -127,4 +138,22 @@ export const TestWithLargeQueryResult = { }, }; +export const TestWithRecommendationPersonActedInMovieQueryResult = { + args: { loading: false }, + play: async () => { + const dispatch = Mockstore.dispatch; + dispatch(assignNewGraphQueryResult(recommendationPersonActedInMovieQueryResultPayload)); + dispatch(setActiveVisualization(Visualizations.Matrix)); + }, +}; + +export const TestWithSlackReactionToThreadedMessageQueryResult = { + args: { loading: false }, + play: async () => { + const dispatch = Mockstore.dispatch; + dispatch(assignNewGraphQueryResult(slackReactionToThreadedMessageQueryResultPayload)); + dispatch(setActiveVisualization(Visualizations.Matrix)); + }, +}; + export default Component; diff --git a/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx b/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx index 689a996ffc9ac2457ac5661fa88c09d27126f5f9..8f1e9032b9948e97a6416c949921a5bc2f68fb86 100644 --- a/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx +++ b/libs/shared/lib/vis/visualizations/matrix/matrixvis.tsx @@ -21,15 +21,29 @@ export const MatrixVis = React.memo(({ data, ml, dispatch, localConfig }: Visual useEffect(() => { if (data) { - setGraph(data); + setGraph( + data + // parseQueryResult(graphQueryResult, ml, { + // defaultX: (ref.current?.clientWidth || 1000) / 2, + // defaultY: (ref.current?.clientHeight || 1000) / 2, + // }) + ); } }, [data, ml]); return ( <> - <div className="h-full w-full overflow-hidden" ref={ref}> - <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} localConfig={localConfig} /> - </div> + <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} localConfig={localConfig} /> + + {/* <VisConfigPanelComponent> */} + {/* <NodeLinkConfigPanelComponent + graph={this.state.graph} + nlViewModel={this.nodeLinkViewModel} + /> */} + {/*</VisConfigPanelComponent>*/} + {/*<VisConfigPanelComponent isLeft>*/} + {/* <AttributesConfigPanel nodeLinkViewModel={this.nodeLinkViewModel} />*/} + {/* </VisConfigPanelComponent> */} </> ); }); diff --git a/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx index 67afb9d1f1ea693f90f9315d55172d930f8c1064..827c543fd1f22e34489fb05ca9ae802c40f9aaaf 100644 --- a/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelink/nodelinkvis.tsx @@ -97,7 +97,6 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch }: VisualizationProp onClickedNode(node, ml); }} /> - {/* <VisConfigPanelComponent> */} {/* <NodeLinkConfigPanelComponent graph={this.state.graph} diff --git a/libs/shared/package.json b/libs/shared/package.json index 13eebf5a69bc7be78bf9f8ea983e662fa716515c..8eccfd0fd607b8b5f4b8c8e25e01df3e840bd089 100644 --- a/libs/shared/package.json +++ b/libs/shared/package.json @@ -24,6 +24,7 @@ "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.11", "@nebula.gl/layers": "^1.0.4", + "@pixi-essentials/cull": "^2.0.0", "@reactflow/node-resizer": "^2.0.1", "@reduxjs/toolkit": "^1.9.2", "@tisoap/react-flow-smart-edge": "^3.0.0", @@ -33,6 +34,7 @@ "@types/supercluster": "^7.1.0", "classnames": "^2.3.2", "color": "^4.2.3", + "color-string": "^1.9.1", "config": "workspace:*", "cytoscape": "^3.23.0", "d3": "^6.6", @@ -50,8 +52,8 @@ "kepler.gl": "^2.5.5", "keycloak-js": "^21.1.1", "lodash-es": "^4.17.21", - "pixi-viewport": "^5.0.2", "pixi-actions": "^1.1.10", + "pixi-viewport": "^5.0.2", "pixi.js": "^7.1.4", "react-color": "^2.19.3", "react-cookie": "^4.1.1", @@ -63,6 +65,7 @@ "react-window": "^1.8.9", "reactflow": "^11.7.0", "regenerator-runtime": "0.13.11", + "reorder.js": "^2.2.6", "sass": "^1.59.3", "scss": "^0.2.4", "styled-components": "^5.3.6", @@ -70,9 +73,7 @@ "tslib": "^2.5.0", "typed": "link:@deck.gl/core/typed", "use-immer": "^0.9.0", - "web-worker": "^1.2.0", - "reorder.js": "^2.2.6", - "color-string": "^1.9.1" + "web-worker": "^1.2.0" }, "devDependencies": { "@iconify/json": "^2.2.95", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5d2313547e6785b6f3ec6823529b2da25daf75c..a73806f1191292fdc44fca6f3483dd4ea43c4021 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,9 @@ importers: '@nebula.gl/layers': specifier: ^1.0.4 version: 1.0.4(@deck.gl/core@8.9.19)(@deck.gl/extensions@8.9.19)(@deck.gl/geo-layers@8.9.19)(@deck.gl/layers@8.9.19)(@deck.gl/mesh-layers@8.9.19)(@luma.gl/constants@8.5.20)(@luma.gl/core@8.5.20) + '@pixi-essentials/cull': + specifier: ^2.0.0 + version: 2.0.0(@pixi/display@7.2.1)(@pixi/math@7.2.1) '@reactflow/node-resizer': specifier: ^2.0.1 version: 2.1.0(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0) @@ -4833,6 +4836,16 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@pixi-essentials/cull@2.0.0(@pixi/display@7.2.1)(@pixi/math@7.2.1): + resolution: {integrity: sha512-6hm1wfCmGItOnyKvCxrmZmOLQVIaN0MqseBweH+tLZH8ecGTIF3qb1cGQDNf9jaK6HH7s/+7m9xXvvk9e92ESw==} + peerDependencies: + '@pixi/display': ^7.0.0 + '@pixi/math': ^7.0.0 + dependencies: + '@pixi/display': 7.2.1(@pixi/core@7.2.1) + '@pixi/math': 7.2.1 + dev: false + /@pixi/accessibility@7.2.1(@pixi/core@7.2.1)(@pixi/display@7.2.1)(@pixi/events@7.2.1): resolution: {integrity: sha512-4kwcmY9CRb07grMd/3FTTpzzpsPlvutnP/xnlkxF8Bz5p2Hrg1nYYxwHzn3hZI0bMIR6oQbScxlx0ijIhVjXrA==} peerDependencies: @@ -16358,7 +16371,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.21 - ts-node: 10.9.1(@types/node@17.0.12)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@18.13.0)(typescript@4.9.5) yaml: 1.10.2 dev: true @@ -20537,8 +20550,8 @@ packages: tinybench: 2.4.0 tinypool: 0.4.0 tinyspy: 1.1.1 - vite: 4.2.1(@types/node@17.0.12)(sass@1.59.3) - vite-node: 0.29.4(@types/node@17.0.12)(sass@1.64.2) + vite: 4.2.1(@types/node@17.0.12)(sass@1.64.2) + vite-node: 0.29.4(@types/node@17.0.12)(sass@1.59.3) why-is-node-running: 2.2.2 transitivePeerDependencies: - less