/* * Copyright (C) 1998-2023 by Northwoods Software Corporation. All Rights Reserved. */ /* * This is an extension and not part of the main GoJS library. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ import * as go from '../release/go.js'; import { PackedLayout } from './PackedLayout.js'; // define an interface for PackedLayout parameters so that they can easily be passed between functions interface PackedLayoutParams { packShape: number; packMode: number; sortMode: number; sortOrder: number; aspectRatio: number; size: go.Size; spacing: number; hasCircularNodes: boolean; } let myDiagram: go.Diagram; let aspectRatio: HTMLInputElement; let layoutWidth: HTMLInputElement; let layoutHeight: HTMLInputElement; let nodeSpacing: HTMLInputElement; let hasCircularNodes: HTMLInputElement; let numNodes: HTMLInputElement; let nodeMinSide: HTMLInputElement; let nodeMaxSide: HTMLInputElement; let sameSides: HTMLInputElement; // nodes need to be randomized again if any of these change let minSidePrevious: number; let maxSidePrevious: number; let sameSidesPrevious: boolean; export function init() { if ((window as any).goSamples) (window as any).goSamples(); // init for these samples -- you don't need to call this const $ = go.GraphObject.make; // for conciseness in defining templates myDiagram = new go.Diagram('myDiagramDiv', // must be the ID or reference to div { 'animationManager.isEnabled': true, layout: $(PackedLayout, { arrangesToOrigin: false }), scale: 0.75, isReadOnly: true }); // Nodes have a template with bindings for size, shape, and fill color myDiagram.nodeTemplate = $(go.Node, 'Auto', new go.Binding('visible', 'visible'), $(go.Shape, {strokeWidth: 0}, new go.Binding('figure', 'figure'), new go.Binding('width', 'width'), new go.Binding('height', 'height'), new go.Binding('fill', 'fill')) ); myDiagram.model = new go.GraphLinksModel([]); // find the elements in the DOM which control configuration aspectRatio = document.getElementById('aspectRatio') as HTMLInputElement; layoutWidth = document.getElementById('width') as HTMLInputElement; layoutHeight = document.getElementById('height') as HTMLInputElement; nodeSpacing = document.getElementById('nodeSpacing') as HTMLInputElement; hasCircularNodes = document.getElementById('hasCircularNodes') as HTMLInputElement; numNodes = document.getElementById('numNodes') as HTMLInputElement; nodeMinSide = document.getElementById('nodeMinSide') as HTMLInputElement; nodeMaxSide = document.getElementById('nodeMaxSide') as HTMLInputElement; sameSides = document.getElementById('sameSides') as HTMLInputElement; aspectRatio.onkeydown = aspectRatioHandler; // create a layout with the default values rebuildGraph(); } // when arrow keys are pressed and the aspect ratio is below 1, increment using the harmonic sequence // this makes the aspect ratio change as follows: 3:1, 2:1, 1:1, 1:2, 1:3, etc. function aspectRatioHandler(e: KeyboardEvent) { if (e.key === 'ArrowUp' && parseFloat(aspectRatio.value) < 1) { e.preventDefault(); const denom = Math.round(1 / parseFloat(aspectRatio.value)); aspectRatio.value = (+(1 / (denom - 1)).toFixed(2)) + ''; rebuildGraph(); } else if (e.key === 'ArrowDown' && parseFloat(aspectRatio.value) <= 1) { e.preventDefault(); const denom = Math.round(1 / parseFloat(aspectRatio.value)); if (denom < 10) { aspectRatio.value = (+(1 / (denom + 1)).toFixed(2)) + ''; rebuildGraph(); } } } function validateInput() { if (!aspectRatio.value || parseFloat(aspectRatio.value) <= 0) { aspectRatio.value = '0.1'; } if (!layoutWidth.value || parseFloat(layoutWidth.value) <= 0) { layoutWidth.value = '1'; } if (!layoutHeight.value || parseFloat(layoutHeight.value) <= 0) { layoutHeight.value = '1'; } if (!nodeSpacing.value) { nodeSpacing.value = '0'; } if (!numNodes.value || parseInt(numNodes.value) < 1) { numNodes.value = '1'; } if (!nodeMinSide.value || parseFloat(nodeMinSide.value) < 1) { nodeMinSide.value = '1'; } if (!nodeMaxSide.value || parseFloat(nodeMaxSide.value) < 1) { nodeMaxSide.value = '1'; } } export function rebuildGraph() { validateInput(); let packShape = PackedLayout.Elliptical; switch (getRadioValue('packShape')) { case 'Elliptical': packShape = PackedLayout.Elliptical; break; case 'Rectangular': packShape = PackedLayout.Rectangular; break; case 'Spiral': packShape = PackedLayout.Spiral; break; } let packMode = PackedLayout.AspectOnly; switch (getRadioValue('packMode')) { case 'AspectOnly': packMode = PackedLayout.AspectOnly; break; case 'Fit': packMode = PackedLayout.Fit; break; case 'ExpandToFit': packMode = PackedLayout.ExpandToFit; break; } let sortMode = PackedLayout.None; switch (getRadioValue('sortMode')) { case 'None': sortMode = PackedLayout.None; break; case 'MaxSide': sortMode = PackedLayout.MaxSide; break; case 'Area': sortMode = PackedLayout.Area; break; } let sortOrder = PackedLayout.Descending; switch (getRadioValue('sortOrder')) { case 'Descending': sortOrder = PackedLayout.Descending; break; case 'Ascending': sortOrder = PackedLayout.Ascending; break; } const params: PackedLayoutParams = { packMode: packMode, packShape: packShape, sortMode: sortMode, sortOrder: sortOrder, aspectRatio: parseFloat(aspectRatio.value), size: new go.Size(parseFloat(layoutWidth.value), parseFloat(layoutHeight.value)), spacing: parseFloat(nodeSpacing.value), hasCircularNodes: hasCircularNodes.checked }; disableInputs(params); if (sameSides.checked !== sameSidesPrevious || parseFloat(nodeMinSide.value) !== minSidePrevious || parseFloat(nodeMaxSide.value) !== maxSidePrevious) { sameSidesPrevious = sameSides.checked; minSidePrevious = parseFloat(nodeMinSide.value); maxSidePrevious = parseFloat(nodeMaxSide.value); randomize(); return; } myDiagram.startTransaction('packed layout'); generateNodeData(); var lay = myDiagram.layout as PackedLayout; lay.packShape = params.packShape; lay.packMode = params.packMode; lay.aspectRatio = params.aspectRatio; lay.size = params.size; lay.spacing = params.spacing; lay.sortOrder = params.sortOrder; lay.sortMode = params.sortMode; lay.hasCircularNodes = params.hasCircularNodes; myDiagram.commitTransaction('packed layout'); } export function randomize() { myDiagram.model = new go.GraphLinksModel([]); rebuildGraph(); } function generateNodeData() { const nodeDataArray = myDiagram.model.nodeDataArray; const count = parseInt(numNodes.value); const min = parseFloat(nodeMinSide.value); const max = parseFloat(nodeMaxSide.value); const shapeToPack = getRadioValue('shapeToPack'); if (count > nodeDataArray.length) { const arr = new Array(); for (let i = nodeDataArray.length; i < count; i++) { const width = Math.floor(Math.random() * (max - min + 1)) + min; const height = sameSides.checked ? width : Math.floor(Math.random() * (max - min + 1)) + min; const color = go.Brush.randomColor(128, 235); arr.push({width: width, height: height, fill: color, figure: shapeToPack}); } myDiagram.model.addNodeDataCollection(arr); } else if (count < nodeDataArray.length) { while (count < nodeDataArray.length) { myDiagram.model.removeNodeData(nodeDataArray[nodeDataArray.length - 1]); } } else { for (const data of nodeDataArray) { myDiagram.model.set(data, 'figure', shapeToPack); } } sameSidesPrevious = sameSides.checked; minSidePrevious = min; maxSidePrevious = max; } let hasCircularNodesSavedState: boolean | null = null; let sameSidesSavedState: boolean | null = null; function disableInputs(params: PackedLayoutParams) { setRadioButtonsDisabled('packMode', params.packShape === PackedLayout.Spiral); aspectRatio.disabled = params.packMode !== PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral; layoutWidth.disabled = params.packMode === PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral; layoutHeight.disabled = params.packMode === PackedLayout.AspectOnly || params.packShape === PackedLayout.Spiral; nodeSpacing.disabled = params.packMode === PackedLayout.ExpandToFit; hasCircularNodes.disabled = params.packShape === PackedLayout.Spiral; if (params.packShape === PackedLayout.Spiral) { if (hasCircularNodesSavedState === null) { hasCircularNodesSavedState = hasCircularNodes.checked; } hasCircularNodes.checked = true; params.hasCircularNodes = true; } else if (hasCircularNodesSavedState !== null) { hasCircularNodes.checked = hasCircularNodesSavedState; params.hasCircularNodes = false; hasCircularNodesSavedState = null; } sameSides.disabled = params.hasCircularNodes; if (params.hasCircularNodes) { if (sameSidesSavedState === null) { sameSidesSavedState = sameSides.checked; } sameSides.checked = true; } else if (sameSidesSavedState !== null) { sameSides.checked = sameSidesSavedState; sameSidesSavedState = null; } } function getRadioValue(name: string): string | undefined { const radio = document.getElementsByName(name); for (let i = 0; i < radio.length; i++) { if ((radio[i] as HTMLInputElement).checked) return (radio[i]as HTMLInputElement).value; } } function setRadioButtonsDisabled(name: string, disabled: boolean) { const radio = document.getElementsByName(name); for (let i = 0; i < radio.length; i++) { (radio[i] as HTMLInputElement).disabled = disabled; } }