/** *******************************************************************************************************************
PRSM Participatory System Mapper
Copyright (C) 2022 Nigel Gilbert prsm@prsm.uk
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
This module handles operations related to the Styles tabs.
******************************************************************************************************************** */
import { Network } from 'vis-network/peer/'
import { DataSet } from 'vis-data/peer'
import {
listen,
elem,
deepMerge,
deepCopy,
standardizeColor,
setNodeHidden,
setEdgeHidden,
dragElement,
statusMsg,
alertMsg,
clearStatusBar,
factorSizeToPercent,
setFactorSizeFromPercent,
convertDashes,
getDashes,
} from './utils.js'
import {
network,
data,
ySamplesMap,
yNetMap,
selectFactors,
selectLinks,
updateLastSamples,
cp,
logHistory,
progressBar,
} from './prsm.js'
import { styles } from './samples.js'
/**
* The samples are each a mini vis-network showing just one node or two nodes and a link
*/
export function setUpSamples() {
// expand the styles object to include the default values
expandStylesObject()
// Get all elements with class='sampleNode' and add listener and canvas
const emptyDataSet = new DataSet([])
let sampleElements = document.getElementsByClassName('sampleNode')
for (let i = 0; i < sampleElements.length; i++) {
const groupId = `group${i}`
const sampleElement = sampleElements[i]
const sampleOptions = styles.nodes[groupId]
const groupLabel = styles.nodes[groupId].groupLabel
const nodeDataSet = new DataSet([
deepMerge({ id: '1', label: groupLabel === undefined ? '' : groupLabel }, sampleOptions, {
chosen: false,
widthConstraint: 50,
heightConstraint: 50,
font: { size: 14 },
size: 50,
margin: 10,
scaling: { label: { enabled: false } },
}),
])
initSample(sampleElement, { nodes: nodeDataSet, edges: emptyDataSet })
sampleElement.addEventListener('dblclick', () => {
editNodeStyle(sampleElement, groupId)
})
sampleElement.addEventListener('click', () => {
updateLastSamples(groupId, null)
})
sampleElement.addEventListener('contextmenu', (event) => {
styleNodeContextMenu(event, sampleElement, groupId)
})
sampleElement.addEventListener('mouseover', () =>
statusMsg(
'Left click: apply style to selected; Double click: edit style; Right click: Select or Hide all with this style'
)
)
sampleElement.addEventListener('mouseout', () => clearStatusBar())
sampleElement.groupNode = groupId
sampleElement.dataSet = nodeDataSet
}
// and to all sampleLinks
sampleElements = document.getElementsByClassName('sampleLink')
for (let i = 0; i < sampleElements.length; i++) {
const sampleElement = sampleElements[i]
const groupId = `edge${i}`
const groupLabel = styles.edges[groupId].groupLabel
const sampleOptions = styles.edges[groupId]
const edgeDataSet = new DataSet([
deepMerge(
{ id: '1', from: 1, to: 2, label: groupLabel === undefined ? '' : groupLabel },
sampleOptions,
{
font: { size: 24, color: 'black', align: 'top', vadjust: -40 },
widthConstraint: 100,
value: 10,
}
),
])
const nodesDataSet = new DataSet([
{ id: 1, size: 5, shape: 'dot', fixed: true, chosen: false },
{ id: 2, size: 5, shape: 'dot', fixed: true, chosen: false },
])
initSample(sampleElement, { nodes: nodesDataSet, edges: edgeDataSet })
sampleElement.addEventListener('dblclick', () => {
editLinkStyle(sampleElement, groupId)
})
sampleElement.addEventListener('click', () => {
updateLastSamples(null, groupId)
})
sampleElement.addEventListener('contextmenu', (event) => {
styleEdgeContextMenu(event, sampleElement, groupId)
})
sampleElement.addEventListener('mouseover', () =>
statusMsg(
'Left click: apply style to selected; Double click: edit style; Right click: Select all with this style'
)
)
sampleElement.groupLink = groupId
sampleElement.dataSet = edgeDataSet
}
// set up color pickers
cp.createColorPicker('nodeStyleEditFillColor', nodeEditSave)
cp.createColorPicker('nodeStyleEditBorderColor', nodeEditSave)
cp.createColorPicker('nodeStyleEditFontColor', nodeEditSave)
cp.createColorPicker('linkStyleEditLineColor', linkEditSave)
// set up listeners
listen('nodeStyleEditCancel', 'click', nodeEditCancel)
listen('nodeStyleEditName', 'keyup', nodeEditSave)
listen('nodeStyleEditShape', 'change', nodeEditSave)
listen('nodeStyleEditBorder', 'change', nodeEditSave)
listen('nodeStyleEditFontSize', 'change', nodeEditSave)
listen('nodeStyleEditSubmit', 'click', nodeEditSubmit)
listen('linkStyleEditCancel', 'click', linkEditCancel)
listen('linkStyleEditName', 'keyup', linkEditSave)
listen('linkStyleEditWidth', 'input', linkEditSave)
listen('linkStyleEditDashes', 'input', linkEditSave)
listen('linkStyleEditArrows', 'change', linkEditSave)
listen('linkStyleEditSubmit', 'click', linkEditSubmit)
listen('styleNodeContextMenuHide', 'contextmenu', (e) => e.preventDefault())
listen('styleNodeContextMenuSelect', 'contextmenu', (e) => e.preventDefault())
listen('styleEdgeContextMenuSelect', 'contextmenu', (e) => e.preventDefault())
}
/**
* assemble styles by merging the specifics into the default
*/
function expandStylesObject() {
let base = styles.nodes.base
for (const prop in styles.nodes) {
const grp = deepMerge(base, styles.nodes[prop])
// make the hover and highlight colors the same as the basic ones
grp.color.highlight = {}
grp.color.highlight.border = grp.color.border
grp.color.highlight.background = grp.color.background
grp.color.hover = {}
grp.color.hover.border = grp.color.border
grp.color.hover.background = grp.color.background
grp.font.size = base.font.size
styles.nodes[prop] = grp
}
base = styles.edges.base
for (const prop in styles.edges) {
const grp = deepMerge(base, styles.edges[prop])
grp.color.highlight = grp.color.color
grp.color.hover = grp.color.color
styles.edges[prop] = grp
}
}
/**
* create the sample network
* @param {HTMLElement} wrapper
* @param {object} sampleData
*/
function initSample(wrapper, sampleData) {
const options = {
interaction: { dragNodes: false, dragView: false, selectable: true, zoomView: false },
manipulation: { enabled: false },
layout: { hierarchical: { enabled: true, direction: 'LR' } },
}
const net = new Network(wrapper, sampleData, options)
net.fit()
net.storePositions()
wrapper.net = net
return net
}
const factorsHiddenByStyle = {}
const linksHiddenByStyle = {}
listen('nodesTab', 'contextmenu', (e) => {
e.preventDefault()
})
listen('linksTab', 'contextmenu', (e) => {
e.preventDefault()
})
/**
* Context menu for node styles
* @param {Event} event
* @param {HTMLElement} sampleElement
* @param {string} groupId
*/
function styleNodeContextMenu(event, sampleElement, groupId) {
const menu = elem('styleNodeContextMenu')
showMenu(event.pageX, event.pageY)
document.addEventListener('click', onClick, false)
function onClick(event) {
// Safari emits a contextmenu and a click event on control-click; ignore the click
if (event.ctrlKey && !event.target.id) return
event.preventDefault()
hideMenu()
document.removeEventListener('click', onClick)
switch (event.target.id) {
case 'styleNodeContextMenuSelect': {
selectFactorsWithStyle(groupId)
break
}
case 'styleNodeContextMenuHide': {
if (sampleElement.dataset.hide !== 'hidden') {
hideFactorsWithStyle(groupId, true)
sampleElement.dataset.hide = 'hidden'
sampleElement.style.opacity = 0.6
} else {
hideFactorsWithStyle(groupId, false)
sampleElement.dataset.hide = 'visible'
sampleElement.style.opacity = 1.0
}
break
}
default: // clicked off menu
break
}
}
function showMenu(x, y) {
elem('styleNodeContextMenuHide').innerText =
sampleElement.dataset.hide === 'hidden' ? 'Unhide Factors' : 'Hide Factors'
if (x + menu.offsetWidth > elem('container').offsetWidth) {
x = elem('container').offsetWidth - menu.offsetWidth
}
if (y + menu.offsetHeight > elem('container').offsetHeight) {
y = elem('container').offsetHeighth - menu.offsetHeight
}
menu.style.left = `${x}px`
menu.style.top = `${y}px`
menu.classList.add('show-menu')
}
function hideMenu() {
menu.classList.remove('show-menu')
}
function selectFactorsWithStyle(groupId) {
selectFactors(data.nodes.getIds({ filter: (node) => node.grp === groupId }))
}
function hideFactorsWithStyle(groupId, toggle) {
const nodes = data.nodes.get({ filter: (node) => node.grp === groupId })
nodes.forEach((node) => {
setNodeHidden(node, toggle)
})
data.nodes.update(nodes)
const edges = []
nodes.forEach((node) => {
const connectedEdges = network.getConnectedEdges(node.id)
connectedEdges.forEach((edgeId) => {
edges.push(data.edges.get(edgeId))
})
})
edges.forEach((edge) => {
setEdgeHidden(edge, toggle)
})
data.edges.update(edges)
factorsHiddenByStyle[sampleElement.id] = toggle
yNetMap.set('factorsHiddenByStyle', factorsHiddenByStyle)
}
}
/**
* Context menu for edge styles
* @param {Event} event
* @param {HTMLElement} sampleElement
* @param {string} groupId
*/
function styleEdgeContextMenu(event, sampleElement, groupId) {
const menu = elem('styleEdgeContextMenu')
showMenu(event.pageX, event.pageY)
document.addEventListener('click', onClick, false)
function onClick(event) {
// Safari emits a contextmenu and a click event on control-click; ignore the click
if (event.ctrlKey && !event.target.id) return
event.preventDefault()
hideMenu()
document.removeEventListener('click', onClick)
switch (event.target.id) {
case 'styleEdgeContextMenuSelect': {
selectLinksWithStyle(groupId)
break
}
case 'styleEdgeContextMenuHide': {
if (sampleElement.dataset.hide !== 'hidden') {
hideLinksWithStyle(groupId, true)
sampleElement.dataset.hide = 'hidden'
sampleElement.style.opacity = 0.6
} else {
hideLinksWithStyle(groupId, false)
sampleElement.dataset.hide = 'visible'
sampleElement.style.opacity = 1.0
}
break
}
default: // clicked off menu
break
}
}
function showMenu(x, y) {
elem('styleEdgeContextMenuHide').innerText =
sampleElement.dataset.hide === 'hidden' ? 'Unhide Links' : 'Hide Links'
if (x + menu.offsetWidth > elem('container').offsetWidth) {
x = elem('container').offsetWidth - menu.offsetWidth
}
if (y + menu.offsetHeight > elem('container').offsetHeight) {
y = elem('container').offsetHeighth - menu.offsetHeight
}
menu.style.left = `${x}px`
menu.style.top = `${y}px`
menu.classList.add('show-menu')
}
function hideMenu() {
menu.classList.remove('show-menu')
}
function selectLinksWithStyle(groupId) {
selectLinks(data.edges.getIds({ filter: (edge) => edge.grp === groupId }))
}
function hideLinksWithStyle(groupId, toggle) {
const edges = data.edges.get({ filter: (edge) => edge.grp === groupId })
edges.forEach((edge) => {
setEdgeHidden(edge, toggle)
})
data.edges.update(edges)
linksHiddenByStyle[sampleElement.id] = toggle
yNetMap.set('linksHiddenByStyle', linksHiddenByStyle)
}
}
/**
* open the dialog to edit a node style
* @param {HTMLElement} styleElement
* @param {number} groupId
*/
function editNodeStyle(styleElement, groupId) {
styleElement.net.unselectAll()
const container = elem('nodeStyleEditorContainer')
container.groupId = groupId
// save the current style format (so that there can be a revert in case of cancelling the edit)
container.origGroup = deepCopy(styles.nodes[groupId])
// save the div in which the style is displayed
container.styleElement = styleElement
// set the style dialog widgets with the current values for the group style
updateNodeEditor(groupId)
// display the style dialog
nodeEditorShow()
}
/**
* ensure that the edit node style dialog shows the current state of the style
* @param {string} groupId
*/
function updateNodeEditor(groupId) {
const group = styles.nodes[groupId]
elem('nodeStyleEditName').value = group.groupLabel !== 'Sample' ? group.groupLabel : ''
elem('nodeStyleEditFillColor').style.backgroundColor = standardizeColor(group.color.background)
elem('nodeStyleEditBorderColor').style.backgroundColor = standardizeColor(group.color.border)
elem('nodeStyleEditFontColor').style.backgroundColor = standardizeColor(group.font.color)
elem('nodeStyleEditShape').value = group.shape
elem('nodeStyleEditBorder').value = getDashes(
group.shapeProperties.borderDashes,
group.borderWidth
)
elem('nodeStyleEditFontSize').value = group.font.size
if (group.fixed) {
elem('nodeStyleEditFixed').style.display = 'inline'
elem('nodeStyleEditUnfixed').style.display = 'none'
} else {
elem('nodeStyleEditFixed').style.display = 'none'
elem('nodeStyleEditUnfixed').style.display = 'inline'
}
elem('nodeStyleEditFactorSize').value = factorSizeToPercent(group.size)
progressBar(elem('nodeStyleEditFactorSize'))
}
listen('nodeStyleEditLock', 'click', toggleNodeStyleLock)
/**
* Toggle the lock state of the node style
*/
function toggleNodeStyleLock() {
const group = styles.nodes[elem('nodeStyleEditorContainer').groupId]
if (group.fixed) {
elem('nodeStyleEditFixed').style.display = 'none'
elem('nodeStyleEditUnfixed').style.display = 'inline'
} else {
elem('nodeStyleEditFixed').style.display = 'inline'
elem('nodeStyleEditUnfixed').style.display = 'none'
}
group.fixed = !group.fixed
}
/**
* save changes to the style made with the edit dialog to the style object
*/
function nodeEditSave() {
const groupId = elem('nodeStyleEditorContainer').groupId
const group = styles.nodes[groupId]
group.groupLabel = elem('nodeStyleEditName').value
if (group.groupLabel === '') group.groupLabel = 'Sample'
group.color.background = elem('nodeStyleEditFillColor').style.backgroundColor
group.color.border = elem('nodeStyleEditBorderColor').style.backgroundColor
group.color.highlight.background = group.color.background
group.color.highlight.border = group.color.border
group.color.hover.background = group.color.background
group.color.hover.border = group.color.border
group.font.color = elem('nodeStyleEditFontColor').style.backgroundColor
group.shape = elem('nodeStyleEditShape').value
const border = elem('nodeStyleEditBorder').value
group.shapeProperties.borderDashes = convertDashes(border)
group.borderWidth = border === 'none' ? 0 : border === 'solid' ? 1 : 4
group.font.size = parseInt(elem('nodeStyleEditFontSize').value)
setFactorSizeFromPercent(group, elem('nodeStyleEditFactorSize').value)
nodeEditUpdateStyleSample(group)
}
/**
* update the style sample to show changes to style
* @param {Object} group
*/
function nodeEditUpdateStyleSample(group) {
const groupId = elem('nodeStyleEditorContainer').groupId
const styleElement = elem('nodeStyleEditorContainer').styleElement
let node = styleElement.dataSet.get('1')
node.label = group.groupLabel
// the node in the style sample does not change size
node = deepMerge(node, styles.nodes[groupId], {
chosen: false,
size: 50,
widthConstraint: 50,
heightConstraint: 50,
margin: 10,
font: { size: 14 },
})
styleElement.dataSet.update(node)
}
/**
* cancel any editing of the style and revert to what it was previously
*/
function nodeEditCancel() {
// restore saved group format
const groupId = elem('nodeStyleEditorContainer').groupId
styles.nodes[groupId] = elem('nodeStyleEditorContainer').origGroup
nodeEditUpdateStyleSample(styles.nodes[groupId])
nodeEditorHide()
}
/**
* hide the node style editor dialog
*/
function nodeEditorHide() {
elem('nodeStyleEditorContainer').classList.add('hideEditor')
}
/**
* show the node style editor dialog
*/
function nodeEditorShow() {
const panelRect = elem('panel').getBoundingClientRect()
const container = elem('nodeStyleEditorContainer')
container.style.top = `${panelRect.top}px`
container.style.left = `${panelRect.left - 300}px`
container.classList.remove('hideEditor')
}
/**
* save the edited style and close the style editor. Update the nodes
* in the map and the legend to the current style
*/
function nodeEditSubmit() {
nodeEditSave()
nodeEditorHide()
// apply updated style to all nodes having that style
const groupId = elem('nodeStyleEditorContainer').groupId
// somewhere - but I have no idea where or why, this is set to true, but it must be false
styles.nodes[groupId].scaling.label.enabled = false
reApplySampleToNodes([groupId], true)
ySamplesMap.set(groupId, { node: styles.nodes[groupId] })
updateLegend()
network.redraw()
logHistory('edited a Factor style')
}
/**
* update all nodes in the map with this style to the current style features
* @param {number[]} groupIds
* @param {boolean} [force] override any existing individual node styling
*/
export function reApplySampleToNodes(groupIds, force) {
const nodesToUpdate = data.nodes.get({
filter: (item) => {
return groupIds.includes(item.grp)
},
})
data.nodes.update(
nodesToUpdate.map((node) => {
return force
? deepMerge(node, styles.nodes[node.grp])
: deepMerge(styles.nodes[node.grp], node)
})
)
}
/**
* ensure that the styles displayed in the node styles panel display the styles defined in the styles array
*/
export function refreshSampleNode(groupId) {
const sampleElements = Array.from(document.getElementsByClassName('sampleNode'))
const sampleElement = sampleElements[groupId.match(/\d+/)?.[0]]
if (!sampleElement) return
let node = sampleElement.dataSet.get()[0]
node = deepMerge(node, styles.nodes[groupId], {
chosen: false,
size: 50,
widthConstraint: 50,
heightConstraint: 50,
margin: 10,
font: { size: 14 },
})
node.label = node.groupLabel
sampleElement.dataSet.remove(node.id)
sampleElement.dataSet.update(node)
sampleElement.net.fit()
}
/**
* open the dialog to edit a link style
* @param {HTMLElement} styleElement
* @param {string} groupId
*/
function editLinkStyle(styleElement, groupId) {
const container = elem('linkStyleEditorContainer')
container.styleElement = styleElement
container.groupId = groupId
// save the current style format (so that there can be a revert in case of cancelling the edit)
container.origGroup = deepCopy(styles.edges[groupId])
// set the style dialog widgets with the current values for the group style
updateLinkEditor(groupId)
// display the style dialog
linkEditorShow()
}
/**
* ensure that the edit link style dialog shows the current state of the style
* @param {string} groupId
*/
function updateLinkEditor(groupId) {
const group = styles.edges[groupId]
elem('linkStyleEditName').value = group.groupLabel !== 'Sample' ? group.groupLabel : ''
elem('linkStyleEditLineColor').style.backgroundColor = standardizeColor(group.color.color)
elem('linkStyleEditWidth').value = group.width
elem('linkStyleEditDashes').value = getDashes(group.dashes, 1)
elem('linkStyleEditArrows').value = getArrows(group.arrows)
}
/**
* save changes to the style made with the edit dialog to the style object
*/
function linkEditSave() {
const groupId = elem('linkStyleEditorContainer').groupId
const group = styles.edges[groupId]
group.groupLabel = elem('linkStyleEditName').value
if (group.groupLabel === '') group.groupLabel = 'Sample'
group.color.color = elem('linkStyleEditLineColor').style.backgroundColor
group.color.highlight = group.color.color
group.color.hover = group.color.color
group.width = parseInt(elem('linkStyleEditWidth').value)
group.dashes = convertDashes(elem('linkStyleEditDashes').value)
groupArrows(elem('linkStyleEditArrows').value)
linkEditUpdateStyleSample(group)
/**
* Set the link object properties to show various arrow types
* @param {string} val
*/
function groupArrows(val) {
if (val !== '') {
group.arrows.from.enabled = false
group.arrows.middle.enabled = false
group.arrows.to.enabled = true
if (val === 'none') group.arrows.to.enabled = false
else group.arrows.to.type = val
}
}
}
/**
* update the style sample to show changes to style
* @param {Object} group
*/
function linkEditUpdateStyleSample(group) {
// update the style edge to show changes to style
const groupId = elem('linkStyleEditorContainer').groupId
const styleElement = elem('linkStyleEditorContainer').styleElement
let edge = styleElement.dataSet.get('1')
edge.label = group.groupLabel
edge = deepMerge(edge, styles.edges[groupId], { chosen: false })
const dataSet = styleElement.dataSet
dataSet.update(edge)
}
/**
* cancel any editing of the style and revert to what it was previously
*/
function linkEditCancel() {
// restore saved group format
const groupId = elem('linkStyleEditorContainer').groupId
styles.edges[groupId] = elem('linkStyleEditorContainer').origGroup
linkEditorHide()
}
/**
* hide the link style editor dialog
*/
function linkEditorHide() {
elem('linkStyleEditorContainer').classList.add('hideEditor')
}
/**
* show the link style editor dialog
*/
function linkEditorShow() {
const panelRect = elem('panel').getBoundingClientRect()
const container = elem('linkStyleEditorContainer')
container.style.top = `${panelRect.top}px`
container.style.left = `${panelRect.left - 300}px`
container.classList.remove('hideEditor')
}
/**
* save the edited style and close the style editor. Update the links
* in the map and the legend to the current style
*/
function linkEditSubmit() {
linkEditSave()
linkEditorHide()
// apply updated style to all edges having that style
const groupId = elem('linkStyleEditorContainer').groupId
reApplySampleToLinks([groupId], true)
ySamplesMap.set(groupId, { edge: styles.edges[groupId] })
updateLegend()
network.redraw()
logHistory('edited a Link style')
}
/**
* update all links in the map with this style to the current style features
* (don't touch cluster edges)
* @param {number[]} groupIds
* @param {boolean} [force] override any existing individual edge styling
*/
export function reApplySampleToLinks(groupIds, force) {
const edgesToUpdate = data.edges.get({
filter: (item) => {
return groupIds.includes(item.grp) && !item.isClusterEdge
},
})
data.edges.update(
edgesToUpdate.map((edge) => {
return force
? deepMerge(edge, styles.edges[edge.grp])
: deepMerge(styles.edges[edge.grp], edge)
})
)
}
/**
* ensure that the styles displayed in the link styles panel display the styles defined in the styles array
*/
export function refreshSampleLink(groupId) {
const sampleElements = Array.from(document.getElementsByClassName('sampleLink'))
const sampleElement = sampleElements[groupId.match(/\d+/)?.[0]]
if (!sampleElement) return
let edge = sampleElement.dataSet.get()[0]
edge = deepMerge(edge, styles.edges[groupId], { chosen: false, value: 10 })
edge.label = edge.groupLabel
sampleElement.dataSet.remove(edge.id)
sampleElement.dataSet.update(edge)
sampleElement.net.fit()
}
/**
* Convert from style object properties to arrow menu selection
* @param {Object} prop
*/
function getArrows(prop) {
let val = 'none'
if (prop.to?.enabled && prop.to.type) val = prop.to.type
return val
}
/* ------------display the map legend (includes all styles with a group label that is neither blank or 'Sample') */
const LEGENDHEIGHT = 35
const LEGENDWIDTH = 120
/**
* display a legend on the map (but only if the styles have been given names)
* @param {Boolean} warn true if user is switching display legend on, but there is nothing to show
*/
export function legend(warn = false) {
clearLegend()
const sampleNodeDivs = document.getElementsByClassName('sampleNode')
const nodes = Array.from(sampleNodeDivs).filter(
(elem) => elem.dataSet.get('1').groupLabel !== 'Sample'
)
const sampleEdgeDivs = document.getElementsByClassName('sampleLink')
const edges = Array.from(sampleEdgeDivs).filter(
(elem) => !['Sample', '', ' '].includes(elem.dataSet.get('1').groupLabel)
)
const nItems = nodes.length + edges.length
if (nItems === 0) {
if (warn) alertMsg('Nothing to include in the Legend - rename some styles first', 'warn')
elem('showLegendSwitch').checked = false
return
}
const legendBox = document.createElement('div')
legendBox.className = 'legend'
legendBox.id = 'legendBox'
elem('main').appendChild(legendBox)
const title = document.createElement('p')
title.id = 'Legend'
title.className = 'legendTitle'
title.appendChild(document.createTextNode('Legend'))
legendBox.appendChild(title)
legendBox.style.height = `${LEGENDHEIGHT * nItems + title.offsetHeight}px`
legendBox.style.width = `${LEGENDWIDTH}px`
const legendWrapper = document.createElement('div')
legendWrapper.className = 'legendWrapper'
legendBox.appendChild(legendWrapper)
dragElement(legendBox, title)
for (let i = 0; i < nodes.length; i++) {
const canvas = document.createElement('div')
canvas.className = 'legendCanvas'
legendWrapper.appendChild(canvas)
const legendData = { nodes: new DataSet(), edges: new DataSet() }
const legendNetwork = new Network(canvas, legendData, {
physics: { enabled: false },
interaction: { zoomView: false, dragView: false },
})
const node = deepMerge(styles.nodes[nodes[i].groupNode])
node.id = i + 10000
node.shape === 'text' ? (node.label = 'groupLabel') : (node.label = '')
node.fixed = true
node.chosen = false
node.margin = 10
node.x = 0
node.y = 0
node.widthConstraint = 10
node.heightConstraint = 10
node.font.size = 10
node.size = 10
legendData.nodes.update(node)
legendNetwork.fit()
const style = document.createElement('div')
style.className = 'legendStyleName'
style.textContent = node.groupLabel
legendWrapper.appendChild(style)
}
for (let i = 0; i < edges.length; i++) {
const canvas = document.createElement('div')
canvas.className = 'legendCanvas'
legendWrapper.appendChild(canvas)
const legendData = { nodes: new DataSet(), edges: new DataSet() }
const legendNetwork = new Network(canvas, legendData, {
physics: { enabled: false },
interaction: { zoomView: false, dragView: false },
})
const edge = deepMerge(styles.edges[edges[i].groupLink])
edge.label = ''
edge.id = i + 10000
edge.from = i + 20000
edge.to = i + 30000
edge.smooth = { type: 'straightCross' }
edge.chosen = false
const nodes = [
{ id: edge.from, size: 5, shape: 'dot', x: -20, y: 0, fixed: true, chosen: false },
{ id: edge.to, shape: 'dot', size: 5, x: +20, y: 0, fixed: true, chosen: false },
]
legendData.nodes.update(nodes)
legendData.edges.update(edge)
legendNetwork.fit()
const style = document.createElement('div')
style.className = 'legendStyleName'
style.textContent = edge.groupLabel
legendWrapper.appendChild(style)
}
}
/**
* remove the legend from the map
*/
export function clearLegend() {
const legendBox = elem('legendBox')
if (legendBox) legendBox.remove()
}
/**
* redraw the legend (to show updated styles)
*/
export function updateLegend() {
if (elem('showLegendSwitch').checked) {
legend(false)
clearStatusBar()
}
}