From c6b3bc1359bbf25eb270004a75a537d3efa5d5ea Mon Sep 17 00:00:00 2001 From: Dennis Collaris <d.collaris@me.com> Date: Wed, 12 Jun 2024 16:04:06 +0200 Subject: [PATCH] feat(vis): remake of matrixVis --- .../pills/customFlowLines/connection.scss | 2 +- .../matrixvis/components/MatrixPixi.tsx | 467 ++++++++++-------- .../components/matrixPixi.module.scss | 37 ++ 3 files changed, 308 insertions(+), 198 deletions(-) create mode 100644 libs/shared/lib/vis/visualizations/matrixvis/components/matrixPixi.module.scss diff --git a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss index 1b9e88a5e..d537570f7 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss +++ b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.scss @@ -1,3 +1,3 @@ -g { +g.react-flow__edge g { @apply stroke-secondary-300; } diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx index a53fd8beb..62383f984 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx @@ -1,20 +1,8 @@ import { Edge, GraphQueryResult, Node, useML, useSearchResultData } from '@graphpolaris/shared/lib/data-access'; import { dataColors, visualizationColors } from 'config'; import { Viewport } from 'pixi-viewport'; -import { - Application, - ColorSource, - Container, - DisplayObject, - FederatedPointerEvent, - Graphics, - IPointData, - Point, - Sprite, - Text, - Texture, -} from 'pixi.js'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { Application, ColorSource, Container, FederatedPointerEvent, Graphics, IPointData, Point, Text } from 'pixi.js'; +import { useEffect, useRef, useState } from 'react'; import { LinkType, NodeType } from '../types'; import { NLPopup } from './MatrixPopup'; @@ -23,10 +11,24 @@ import { Actions, Interpolations } from 'pixi-actions'; import Color from 'color'; import { createColumn } from './ColumnGraphicsComponent'; import { ReorderingManager } from './ReorderingManager'; -import { VisualizationSettingsType } from '../../../common'; -import { range, scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3'; +import { + select, + selectAll, + range, + scaleLinear, + scaleBand, + type ScaleBand, + axisTop, + axisLeft, + easeCubicOut, + type Selection, + type BaseType, + type Axis, +} from 'd3'; import { MatrixVisProps } from '../matrixvis'; +import styles from './matrixPixi.module.scss'; + type Props = { // onClick: (node: NodeType, pos: IPointData) => void; // onHover: (data: { node: NodeType; pos: IPointData }) => void; @@ -39,19 +41,15 @@ type Props = { }; const columnsContainer = new Container(); -const columnAxisContainer = new Container(); -const rowsContainer = new Container(); ////////////////// // MAIN COMPONENT ////////////////// export const MatrixPixi = (props: Props) => { - const app = useMemo(() => new Application({ background: 0xffffff, antialias: true, autoDensity: true, eventMode: 'auto' }), []); - let config = { - textOffsetX: 100, - textOffsetY: 100, + textOffsetX: 50, + textOffsetY: 50, visMapping: [] as any[], // TODO type cellHeight: 100, @@ -65,17 +63,21 @@ export const MatrixPixi = (props: Props) => { 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>()); const viewport = useRef<Viewport>(); const ref = useRef<HTMLDivElement>(null); + const canvas = useRef<HTMLCanvasElement>(null); + const svg = useRef<SVGSVGElement>(null); const isSetup = useRef(false); const ml = useML(); const searchResults = useSearchResultData(); // const imperative = useRef<any>(null); + let app: Application; + function resize() { + if (app == null) return; + const width = ref?.current?.clientWidth || 1000; const height = ref?.current?.clientHeight || 1000; app.renderer.resize(width, height); @@ -102,13 +104,6 @@ export const MatrixPixi = (props: Props) => { return () => resizeObserver.disconnect(); // clean up }, []); - useEffect(() => { - if (ref.current && ref.current.children.length === 0) { - ref.current.appendChild(app.view as HTMLCanvasElement); - resize(); - } - }, [ref]); - useEffect(() => { // 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) { @@ -144,7 +139,7 @@ export const MatrixPixi = (props: Props) => { // }, [searchResults]); function onButtonDown(event: FederatedPointerEvent) { - console.log( + console.debug( event.currentTarget, // graph.nodes.find((node) => node.id === event.currentTarget.name) ); @@ -156,26 +151,22 @@ export const MatrixPixi = (props: Props) => { if (props.graph) { if (forceClear) { - nodeMap.current.clear(); - linkMap.current.clear(); - rowsContainer.removeChildren(); columnsContainer.removeChildren(); } } }; - const onHoverColumn = (event: any, currentNode: Node | null) => { - app.renderer.plugins.interaction.cursorStyles.pointer = 'crosshair'; + const onHoverColumn = (event: any, currentNode: Node | null | undefined) => { + if (Actions.actions.length > 0) { + // Don't hover if reorder animation are running, causes glitches. + return; + } + if (!currentNode) { - rowsContainer.children.forEach((row) => { - row.alpha = 1; - }); columnsContainer.children.forEach((col) => { col.alpha = 1; }); - columnAxisContainer.children.forEach((colText) => { - colText.alpha = 1; - }); + selectAll('.axis .tick').style('opacity', 1); return; } @@ -185,53 +176,32 @@ export const MatrixPixi = (props: Props) => { }); event.currentTarget.alpha = 1; - columnAxisContainer.children.forEach((colText) => { - colText.alpha = 0.25; - if (colText.name?.endsWith(currentNode._id)) colText.alpha = 1; - }); - if (!props.graph) return; - rowsContainer.children.forEach((col) => { - col.alpha = 0.25; - }); - - if (!props.graph) return; - rowsContainer.children.forEach((col) => { - col.alpha = 0.25; - }); const edgesForThisColumn = props.graph.edges.filter((edge) => { return edge.from === currentNode._id || edge.to === currentNode._id; }); - edgesForThisColumn.forEach((edge) => { - let row = rowsContainer.getChildByName(edge.to); - if (row) row.alpha = 1; + selectAll('.axis.left .tick').style('opacity', (d) => { + const highlight = edgesForThisColumn.some((edge) => edge.from == d || edge.to == d); + return highlight ? 1 : 0.25; + }); - row = rowsContainer.getChildByName(edge.from); - if (row) row.alpha = 1; + selectAll('.axis.top .tick').style('opacity', (d) => { + const highlight = edgesForThisColumn.some((edge) => edge.from == d || edge.to == d); + return highlight ? 1 : 0.25; }); }; const reorderColumns = (columnOrder: string[]) => { - const columns = columnsContainer.children; - const columnsText = columnAxisContainer.children; - let columnPositions: Point[] = []; - for (let i = 0; i < columns.length; i++) { - const col = columns[i]; - columnPositions.push(new Point(col.position.x, col.position.y)); - } - - let columnTextPositions: Point[] = []; - for (let i = 0; i < columnsText.length; i++) { - const col = columnsText[i]; - columnTextPositions.push(new Point(col.position.x, col.position.y)); + if (Actions.actions.length > 0) { + // Don't reorder if previous animation is still running, causes glitches. + return; } - if (columnTextPositions.length !== columnPositions.length) - throw new Error( - 'columnTextPositions and columnPositions have different length ' + columnTextPositions.length + ' / ' + columnPositions.length, - ); + // Animate columns + const columns = columnsContainer.children; + let columnPositions = columnOrder.map((_, i) => new Point(config.textOffsetX + i * config.cellWidth, columns[i].position.y)); for (let i = 0; i < columns.length; i++) { const column = columns[i]; @@ -240,53 +210,67 @@ export const MatrixPixi = (props: Props) => { if (index === -1) throw new Error('column not found in columnOrder'); const newPosition = columnPositions[index]; - // move column to new position Actions.moveTo(columns[i], newPosition.x, newPosition.y, 1, Interpolations.pow2out).play(); - - const newPositionText = columnTextPositions[index]; - // move column to new position - Actions.moveTo(columnsText[i], newPositionText.x, newPositionText.y, 1, Interpolations.pow2out).play(); } - }; - const reorderRows = ( - nodesCol: Node[], - nodesRow: Node[], - edges: Edge[], - rowOrder: string[], - colorScale: any, - cellWidth: number, - cellHeight: number, - ) => { - const rows = rowsContainer.children; - let rowPositions: Point[] = []; - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - rowPositions.push(new Point(row.position.x, row.position.y)); - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i] as DisplayObject; - if (!row.name) throw new Error('row has no name'); - const index = rowOrder.indexOf(row.name); - if (index === -1) throw new Error('row not found in rowOrder'); + // Animate column axis + scaleColumns.domain(columnOrder); + axisColumns.scale(scaleColumns); + + animatingAxes = true; + const selectionTop = select(svg.current).select('.axis.top') as Selection<SVGGElement, unknown, BaseType, unknown>; + selectionTop + .transition() + .duration(1000) + .ease(easeCubicOut) + .on('end', () => (animatingAxes = false)) + .call(axisColumns); + }; - const newPosition = rowPositions[index]; - // move column to new position - Actions.moveTo(row, newPosition.x, newPosition.y, 1, Interpolations.pow2out).play(); + const reorderRows = (edges: Edge[], rowOrder: string[], columnOrder: string[]) => { + if (Actions.actions.length > 0) { + // Don't reorder if previous animation is still running, causes glitches. + return; } - const colOrder = columnsContainer.children.map((col) => col.name) as string[]; - if (!colOrder) throw new Error('colOrder is undefined'); + // Rearrange rows columnsContainer.removeChildren(); - setupColumns(edges, colOrder, rowOrder); + setupColumns(edges, columnOrder, rowOrder); + const cols = columnOrder.flatMap((x) => props.graph?.nodes.filter((n) => n._id == x) ?? []); + setupColumnInteractivity(cols); + + // Animate row axis + scaleColumns.domain(columnOrder); + scaleRows.domain(rowOrder); + + animatingAxes = true; + const selectionLeft = select(svg.current).select('.axis.left') as Selection<SVGGElement, unknown, BaseType, unknown>; + selectionLeft + .transition() + .duration(1000) + .ease(easeCubicOut) + .on('end', () => (animatingAxes = false)) + .call(axisRows); }; const setup = () => { if (!props.graph) throw Error('Graph is undefined; setup not possible'); + + if (app == null) { + app = new Application({ + background: 0xffffff, + antialias: true, + autoDensity: true, + eventMode: 'auto', + resolution: window.devicePixelRatio || 1, + view: canvas.current as HTMLCanvasElement, + }); + } + + if (svg.current != null) { + select(svg.current).select('*').remove(); + } columnsContainer.removeChildren(); - rowsContainer.removeChildren(); - columnAxisContainer.removeChildren(); app.stage.removeChildren(); const size = ref.current?.getBoundingClientRect(); @@ -302,7 +286,6 @@ export const MatrixPixi = (props: Props) => { app.stage.addChild(viewport.current); viewport.current.addChild(columnsContainer); - viewport.current.addChild(rowsContainer); const groupByType = props.graph.nodes.reduce( (group: any, node: Node) => { @@ -348,12 +331,20 @@ export const MatrixPixi = (props: Props) => { setupVisualizationEncodingMapping(props.settings); + if (svg.current != null && canvas.current != null) { + select(svg.current) + .attr('width', canvas.current.clientWidth) + .attr('height', canvas.current.clientHeight) + .style('position', 'absolute') + .style('top', 0) + .style('left', 0) + .style('pointer-events', 'none'); + } + setupColumns(props.graph.edges, columnOrder, rowOrder); - setupColumnLegend(columnOrder); + setupColumnAxis(columnOrder, cols[0].label); setupColumnInteractivity(cols); - - setupRowLegend(rows, rowOrder); - setupRowInteractivity(cols, rows, rowOrder, scaleOrdinal(schemeCategory10)); + setupRowAxis(rowOrder, rows[0].label); console.debug('setup matrixvis with graph:', props.graph); @@ -365,6 +356,9 @@ export const MatrixPixi = (props: Props) => { isSetup.current = true; }; + let scaleColumns: ScaleBand<string>; + let scaleRows: ScaleBand<string>; + const setupVisualizationEncodingMapping = (settings: MatrixVisProps) => { if (!props.graph) throw new Error('Graph is undefined; cannot setup matrix'); const visMapping = []; // TODO type @@ -380,17 +374,25 @@ export const MatrixPixi = (props: Props) => { encoding: settings.marks, colorScale: adjacenyScale, renderFunction: function (i: number, color: ColorSource, gfxContext: Graphics) { - gfxContext.beginFill(color, 1); + // Clear locally + gfxContext.beginFill(0xffffff, 1); + gfxContext.drawRect(0, i * config.cellHeight, config.cellWidth, config.cellHeight); + gfxContext.endFill(); + gfxContext.beginFill(color, 1); // TODO get // gfx.beginFill(colorScale(node.type))); - gfxContext.lineStyle(2, 0xffffff); + const inset = 0.5; if (this.encoding === 'rect') { - gfxContext.drawRect(0, i * config.cellHeight, config.cellWidth, config.cellHeight); + gfxContext.drawRect(inset, i * config.cellHeight + inset, config.cellWidth - 2 * inset, config.cellHeight - 2 * inset); } if (this.encoding === 'circle') { - gfxContext.drawCircle(config.cellWidth / 2, i * config.cellHeight + config.cellHeight / 2, config.cellWidth / 2); + gfxContext.drawCircle( + config.cellWidth / 2 + inset, + i * config.cellHeight + config.cellHeight / 2 + inset, + config.cellWidth / 2 - inset, + ); } gfxContext.endFill(); @@ -454,44 +456,32 @@ export const MatrixPixi = (props: Props) => { oneColumn.on('pointerdown', onButtonDown); oneColumn.on('pointerover', (event) => { const col = cols.find((col) => col._id === oneColumn.name); - if (col) onHoverColumn(event, col); + onHoverColumn(event, col); }); oneColumn.on('pointerout', (event) => { - onHoverColumn(event, null as any); + onHoverColumn(event, null); }); } }; - const setupColumnLegend = (columnOrder: string[]) => { - // columnAxisContainer.removeChildren(); - columnAxisContainer.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); - } - viewport.current?.addChild(columnAxisContainer); - - const columnAxis = new Sprite(Texture.WHITE); - columnAxis.width = columnOrder.length * config.cellWidth; - columnAxis.height = config.textOffsetY; - columnAxis.tint = 0xaaaaaa; - columnAxis.position.set(config.textOffsetX, 0); - columnAxis.alpha = 0.0; - columnAxis.interactive = true; - columnAxis.on('pointerdown', (event) => { - // const reordering = new ReorderColumnsAction(columnOrder); - + const setupColumnAxis = (columnOrder: string[], label: string) => { + if (scaleColumns == null) scaleColumns = scaleBand(); + scaleColumns.domain(columnOrder); + + if (svg.current == null) return; + const selection = select(svg.current); + const g = selection.append('g').attr('class', 'axis top').attr('transform', `translate(0, ${config.textOffsetY})`); + g.append('text') + .attr('class', 'label') + .style('text-anchor', 'middle') + .style('alignment-baseline', 'hanging') + .style('fill', 'black') + .style('font-size', '14') + .text(label); + + // Click handler for reordering columns + const axisTopHandle = ref.current?.querySelector(`.${styles.axisTop}`) as HTMLDivElement; + axisTopHandle.addEventListener('click', () => { if (!props.graph) throw new Error('Graph is undefined; cannot reorder matrix'); const reorderingManager = new ReorderingManager(props.graph); @@ -500,14 +490,56 @@ export const MatrixPixi = (props: Props) => { reorderColumns(columnOrder); }); - // columnAxis.on('pointerdown', reorderColumns); - columnAxis.on('pointerover', (event) => { - Actions.fadeTo(columnAxis, 0.5, 0.5, Interpolations.pow2out).play(); - }); - columnAxis.on('pointerout', (event) => { - Actions.fadeTo(columnAxis, 0.0, 0.5, Interpolations.pow2out).play(); - }); - viewport.current?.addChild(columnAxis); + + // Create d3 axis object + axisColumns = axisTop(scaleColumns) + .tickSizeOuter(0) + .tickFormat((d: string, i: number) => { + const minimumWidth = 25; + const range = scaleColumns.range()[1] - scaleColumns.range()[0]; + let skip = Math.round((minimumWidth * columnOrder.length) / range); + skip = Math.max(1, skip); + + return i % skip === 0 ? d : ''; + }); + + // Ensure axis is updated + updateColumnAxis(); + }; + + let axisColumns: Axis<string>; + let axisRows: Axis<string>; + + let animatingAxes = false; + const updateColumnAxis = () => { + if (viewport.current == null || canvas.current == null) return; + + const initialWidth = columnOrder.length * config.cellWidth + 2 * config.textOffsetX; + + scaleColumns.range([ + viewport.current.position.x + viewport.current.scale.x * config.textOffsetX, + viewport.current.position.x + viewport.current.scale.x * (initialWidth - config.textOffsetX), + ]); + + if (svg.current == null) return; + + if (animatingAxes) return; + + const selectionTop = select(svg.current).select('.axis.top') as Selection<SVGGElement, unknown, BaseType, unknown>; + if (!selectionTop.empty()) { + selectionTop.call(axisColumns); + selectionTop + .select('text.label') + .attr('x', function () { + const halfWidth = (this as SVGTextElement).getBBox().width / 2; + return Math.max( + config.textOffsetX + halfWidth, + Math.min((svg.current?.clientWidth ?? 0) - halfWidth, (scaleColumns.range()[1] + scaleColumns.range()[0]) / 2), + ); + }) + .attr('y', -config.textOffsetY + 7); + selectionTop.select('.domain').style('display', 'none'); + } }; function shuffle(array: any[]) { @@ -529,48 +561,84 @@ export const MatrixPixi = (props: Props) => { return array; } - function setupRowLegend(rows: Node[], rowOrder: string[]) { - 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); + function setupRowAxis(rowOrder: string[], label: string) { + if (scaleRows == null) scaleRows = scaleBand(); + scaleRows.domain(rowOrder); + + if (svg.current == null) return; + const selection = select(svg.current); + const g = selection.append('g').attr('class', 'axis left').attr('transform', `translate(${config.textOffsetX}, 0)`); + + g.append('text') + .attr('class', 'label') + .style('text-anchor', 'middle') + .style('alignment-baseline', 'hanging') + .style('fill', 'black') + .style('font-size', '14') + .text(label); + + // Click handler for reordering columns + const axisLeftHandle = ref.current?.querySelector(`.${styles.axisLeft}`) as HTMLDivElement; + axisLeftHandle.addEventListener('click', () => { + if (!props.graph) throw new Error('Graph is undefined; cannot reorder matrix'); - Actions.sequence(Actions.delay(i * 0.005 * Math.random()), Actions.fadeIn(basicText, 0.5, Interpolations.pow2out)).play(); - } + rowOrder = shuffle(rowOrder); + reorderRows(props?.graph.edges, rowOrder, columnOrder); + }); + + // Create d3 axis object + axisRows = axisLeft(scaleRows) + .tickSizeOuter(0) + .tickFormat((d: string, i: number) => { + const minimumHeight = 20; + const range = scaleRows.range()[1] - scaleRows.range()[0]; + let skip = Math.round((minimumHeight * rowOrder.length) / range); + skip = Math.max(1, skip); + + return i % skip === 0 ? d : ''; + }); + + // Ensure axis is updated + updateRowAxis(); } - 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.tint = 0xaaaaaa; - rowAxis.position.set(0, config.textOffsetY); - rowAxis.alpha = 0.0; - rowAxis.interactive = true; - rowAxis.on('pointerdown', (event) => { - if (props.graph) { - rowOrder = shuffle(rowOrder); - reorderRows(cols, rows, props?.graph.edges, rowOrder, colorScale, config.cellWidth, config.cellHeight); - } - }); - rowAxis.on('pointerover', (event) => { - Actions.fadeTo(rowAxis, 0.5, 0.5, Interpolations.pow2out).play(); - }); - rowAxis.on('pointerout', (event) => { - Actions.fadeTo(rowAxis, 0.0, 0.5, Interpolations.pow2out).play(); - }); - viewport.current?.addChild(rowAxis); + const updateRowAxis = () => { + if (viewport.current == null || canvas.current == null) return; + + const initialHeight = rowOrder.length * config.cellHeight + 2 * config.textOffsetY; + + scaleRows.range([ + viewport.current.position.y + viewport.current.scale.y * config.textOffsetY, + viewport.current.position.y + viewport.current.scale.y * (initialHeight - config.textOffsetY), + ]); + + if (svg.current == null) return; + + if (animatingAxes) return; + + const selectionLeft = select(svg.current).select('.axis.left') as Selection<SVGGElement, unknown, BaseType, unknown>; + if (!selectionLeft.empty()) { + selectionLeft.call(axisRows); + selectionLeft.select('text.label').attr('transform', function () { + const halfHeight = (this as SVGTextElement).getBBox().width / 2; + const y = Math.max( + config.textOffsetY + halfHeight, + Math.min((ref.current?.clientHeight ?? 0) - halfHeight, (scaleRows.range()[1] + scaleRows.range()[0]) / 2), + ); + + return `translate( + ${-config.textOffsetX + 7}, + ${y})rotate(-90) + `; + }); + selectionLeft.select('.domain').style('display', 'none'); + } }; const tick = (delta: number) => { if (props.graph) { + updateColumnAxis(); + updateRowAxis(); Actions.tick(delta / 60); } }; @@ -581,7 +649,12 @@ export const MatrixPixi = (props: Props) => { <NLPopup onClose={() => {}} data={popup} key={popup.node.id} /> ))} {quickPopup && <NLPopup onClose={() => {}} data={quickPopup} />} - <div className="h-full w-full overflow-hidden" ref={ref}></div> + <div ref={ref} className={`h-full w-full overflow-hidden relative ${styles.matrix}`}> + <canvas ref={canvas}></canvas> + <div className={styles.axisLeft}></div> + <div className={styles.axisTop}></div> + <svg ref={svg}></svg> + </div> </> ); }; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/matrixPixi.module.scss b/libs/shared/lib/vis/visualizations/matrixvis/components/matrixPixi.module.scss new file mode 100644 index 000000000..b41a7d661 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/matrixvis/components/matrixPixi.module.scss @@ -0,0 +1,37 @@ +.matrix { + --size: 50px; +} + +.axisLeft, .axisTop { + position: absolute; + left: 0; + top: 0; + backdrop-filter: blur(10px); + background: rgba(255,255,255, 0.5); +} + +.axisLeft { + top: var(--size); + bottom: 0; + width: var(--size); + box-shadow: 1px 0px 0px 0px rgba(0,0,0,0.2); +} + +.axisTop { + right: 0; + height: var(--size); + box-shadow: var(--size) 1px 0px 0px rgba(0,0,0,0.2); +} + +/* mask top left corner */ +.axisTop + svg { + clip-path: polygon( + 0% 0%, + 0% 100%, + 100% 100%, + 100% 0%, + var(--size) 0%, + var(--size) var(--size), + 0% var(--size) + ); +} \ No newline at end of file -- GitLab