diff --git a/includes/KnowledgeGraph.php b/includes/KnowledgeGraph.php index c1ca188..f8b56f3 100644 --- a/includes/KnowledgeGraph.php +++ b/includes/KnowledgeGraph.php @@ -222,6 +222,7 @@ public static function parserFunctionKnowledgeGraph( Parser $parser, ...$argv ) 'show-property-type' => [ 'false', 'boolean' ], 'properties-panel' => [ 'false', 'boolean' ], 'categories-panel' => [ 'false', 'boolean' ], + 'palette' => [ 'default', 'string' ], ]; self::initSMW(); @@ -279,9 +280,14 @@ public static function parserFunctionKnowledgeGraph( Parser $parser, ...$argv ) $out->setExtensionData( 'knowledgegraphs', self::$graphs ); + $paletteName = $params['palette'] ?? 'default'; + $colors = $GLOBALS['wgKnowledgeGraphColorPalettes'][$paletteName] + ?? $GLOBALS['wgKnowledgeGraphColorPalettes']['default']; + $out->addJsConfigVars( [ 'KnowledgeGraphShowImages' => $GLOBALS['wgKnowledgeGraphShowImages'], - 'KnowledgeGraphDisableCredits' => $GLOBALS['wgKnowledgeGraphDisableCredits'] + 'KnowledgeGraphDisableCredits' => $GLOBALS['wgKnowledgeGraphDisableCredits'], + 'wgKnowledgeGraphColorPalette' => $colors ] ); return [ diff --git a/includes/specials/SpecialKnowledgeGraphDesigner.php b/includes/specials/SpecialKnowledgeGraphDesigner.php index 6431e84..42e615c 100644 --- a/includes/specials/SpecialKnowledgeGraphDesigner.php +++ b/includes/specials/SpecialKnowledgeGraphDesigner.php @@ -60,10 +60,15 @@ public function execute( $par ) { \KnowledgeGraph::$graphs[] = $params; + $paletteName = $params['palette'] ?? 'default'; + $colors = $GLOBALS['wgKnowledgeGraphColorPalettes'][$paletteName] + ?? $GLOBALS['wgKnowledgeGraphColorPalettes']['default']; + $out->addJsConfigVars( [ 'knowledgegraphs' => json_encode( \KnowledgeGraph::$graphs ), 'KnowledgeGraphShowImages' => $GLOBALS['wgKnowledgeGraphShowImages'], - 'KnowledgeGraphDisableCredits' => $GLOBALS['wgKnowledgeGraphDisableCredits'] + 'KnowledgeGraphDisableCredits' => $GLOBALS['wgKnowledgeGraphDisableCredits'], + 'wgKnowledgeGraphColorPalette' => $colors ] ); $out->addHTML( diff --git a/resources/KnowledgeGraph.css b/resources/KnowledgeGraph.css index 1fc6cd9..a3d3dce 100644 --- a/resources/KnowledgeGraph.css +++ b/resources/KnowledgeGraph.css @@ -63,9 +63,29 @@ padding: 4px; } -.custom-menu li.custom-menu-link-entry:hover, -.custom-menu li.custom-menu-property-entry:hover, -.custom-menu li.custom-menu-edge-entry:hover { - background-color: #e0f0ff; - cursor: pointer; +.kg-node-properties-menu li.kg-node-properties-menu-link-entry:hover, +.kg-node-properties-menu li.kg-node-properties-menu-property-entry:hover, +.kg-node-properties-menu li.kg-node-properties-menu-edge-entry:hover { + background-color: #e0f0ff; + cursor: pointer; +} + +.kg-node-properties-menu { + position: absolute; + background: #fff; + border: 1px solid #ccc; + padding: 5px; + list-style: none; + z-index: 10000; + max-height: 300px; + overflow-y: auto; + margin: 0; + box-shadow: 0 4px 10px rgba(0,0,0,0.1); + cursor: pointer; +} + +.kg-node-properties-menu-property-entry-selected { + font-style: italic; + font-weight: bold; + color: #2B7CE9; } diff --git a/resources/KnowledgeGraph.js b/resources/KnowledgeGraph.js index bb33907..6c0dbbc 100644 --- a/resources/KnowledgeGraph.js +++ b/resources/KnowledgeGraph.js @@ -28,38 +28,54 @@ KnowledgeGraph = function () { var LegendDiv; var PropIdPropLabelMap = {}; var nodePropertiesCache = {}; + const colors = mw.config.get('wgKnowledgeGraphColorPalette'); function addLegendEntry(id, label, color) { + if (!LegendDiv) return; if ($(LegendDiv).find('#' + id.replace(/ /g, '_')).length) { return; } - + let fontColor = KnowledgeGraphFunctions.getContrastColor(color); // koristi istu WCAG logiku + if (!fontColor) fontColor = '#000000'; + var container = document.createElement('button'); container.className = 'legend-element-container'; container.classList.add('btn', 'btn-outline-light'); container.id = id.replace(/ /g, '_'); - container.style.color = 'black'; + container.style.color = fontColor; container.style.background = color; container.innerHTML = label; + container.innerHTML = id; container.dataset.active = true; container.dataset.active_color = color; - container.addEventListener('click', (event) => - dispatchEvent_LegendClick(event, id) - ); - LegendDiv.append(container); } + function checkAndToogleId(id) { + return id.trim().replace(/_/g, ' ').replace(/#.*$/, ''); + } + function dispatchEvent_LegendClick(event, id) { var container = $(LegendDiv).find('#' + id.replace(/ /g, '_'))[0]; if (container.dataset.active === 'true') { container.dataset.active = false; container.style.background = '#FFFFFF'; + + let bgColor = container.style.background; + let fontColor = KnowledgeGraphFunctions.getContrastColor(bgColor); + if (!fontColor) fontColor = '#000000'; + container.style.color = fontColor; + } else { container.dataset.active = true; container.style.background = container.dataset.active_color; + + let bgColor = container.style.background; + let fontColor = KnowledgeGraphFunctions.getContrastColor(bgColor); + if (!fontColor) fontColor = '#000000'; + container.style.color = fontColor; } var updateNodes = []; var visited = []; @@ -78,7 +94,7 @@ KnowledgeGraph = function () { var found = false; connectedEdges.forEach((edge) => { - if (edge.to === nodeId) { + if (edge.to === nodeId || edge.from === nodeId) { found = true; } }); @@ -94,7 +110,11 @@ KnowledgeGraph = function () { } Nodes.forEach((node) => { - if (PropIdPropLabelMap[id].indexOf(node.id) !== -1) { + var idValue = checkAndToogleId(node.id); + if (PropIdPropLabelMap[id] === undefined) { + PropIdPropLabelMap[id] = []; + } + if (PropIdPropLabelMap[id].indexOf(idValue) !== -1 || PropIdPropLabelMap[id].indexOf(node.id) !== -1) { updateNodes.push({ id: node.id, hidden: container.dataset.active === 'true' ? false : true, @@ -210,6 +230,23 @@ KnowledgeGraph = function () { return panel.$element.get(0); } + function wrapLabel(text, maxLength) { + const words = text.split(' '); + let wrapped = ''; + let line = ''; + + for (let word of words) { + if ((line + word).length > maxLength) { + if (line) wrapped += line.trim() + '\n'; + line = word + ' '; + } else { + line += word + ' '; + } + } + wrapped += line.trim(); + return wrapped; + } + function addArticleNode(data, label, options, typeID) { if (Nodes.get(label) !== null) { return; @@ -225,7 +262,7 @@ KnowledgeGraph = function () { label: cleanLabel.length <= maxPropValueLength ? cleanLabel - : cleanLabel.substring(0, maxPropValueLength) + '…', + : wrapLabel(cleanLabel, 20), shape: 'box', font: jQuery.extend( {}, @@ -290,19 +327,29 @@ KnowledgeGraph = function () { var property = data[label].properties[i]; if (!(property.canonicalLabel in PropColors)) { - var color_; - function colorExists() { - for (var j in PropColors) { - if (PropColors[j] === color_) { - return true; + if (colors && colors.length > 0) { + // use d3 palette colors defined in wgKnowledgeGraphColorPalette + PropColors[property.canonicalLabel] = KnowledgeGraphFunctions.colorForPropertyLabel( + property.canonicalLabel, + colors, + PropColors + ); + } else { + // use random HSL colors if no palette defined + let color_; + function colorExists() { + for (let j in PropColors) { + if (PropColors[j] === color_) { + return true; + } } + return false; } - return false; + do { + color_ = KnowledgeGraphFunctions.randomHSL(); + } while (colorExists()); + PropColors[property.canonicalLabel] = color_; } - do { - color_ = KnowledgeGraphFunctions.randomHSL(); - } while (colorExists()); - PropColors[property.canonicalLabel] = color_; } var options = @@ -316,7 +363,22 @@ KnowledgeGraph = function () { options = options.nodes; } if (!('color' in options)) { - options.color = PropColors[property.canonicalLabel]; + const nodeColor = PropColors[property.canonicalLabel]; + const textColor = KnowledgeGraphFunctions.getContrastColor(nodeColor); + + options.color = { + background: nodeColor, + border: '#333', + highlight: { + background: nodeColor, + border: '#000' + } + }; + + // ensure readable font color when node background is dark + options.font = Object.assign({}, options.font, { + color: textColor + }); } var legendLabel = @@ -406,7 +468,7 @@ KnowledgeGraph = function () { if (!Nodes.get(valueId)) { const displayLabel = targetLabel.length <= maxPropValueLength ? targetLabel - : targetLabel.substring(0, maxPropValueLength) + '…'; + : wrapLabel(targetLabel, 20); Nodes.add( jQuery.extend({}, options, { @@ -920,23 +982,21 @@ ${propertyOptions}|show-property-type=true } function findNodeIdContaining(labelPart) { - const allNodes = Nodes.get(); - for (let node of allNodes) { - const nodeLabel = node.id.split('#')[0]; - if (nodeLabel === labelPart) { - return node.id; + const allNodes = Nodes.get(); + for (let node of allNodes) { + const nodeLabel = node.id.split('#')[0]; + if (nodeLabel === labelPart) { + return node.id; + } } + return null; } - return null; -} - - function attachContextMenuListener() { Network.on('oncontext', function (params) { params.event.preventDefault(); // close custom menu if exists - $('.custom-menu').hide(); + $('.kg-node-properties-menu').hide(); const pointer = { x: params.pointer.DOM.x, y: params.pointer.DOM.y }; const edgeId = Network.getEdgeAt(pointer); @@ -946,22 +1006,10 @@ ${propertyOptions}|show-property-type=true return; } - // create custom-menu if not exists - let $menu = $('.custom-menu'); + // create kg-node-properties-menu if not exists + let $menu = $('.kg-node-properties-menu'); if (!$menu.length) { - $menu = $('
').appendTo('body').hide().css({ - position: 'absolute', - background: '#fff', - border: '1px solid #ccc', - padding: '5px', - listStyle: 'none', - zIndex: 10000, - maxHeight: '300px', - overflowY: 'auto', - margin: 0, - boxShadow: '0 4px 10px rgba(0,0,0,0.1)', - cursor: 'pointer' - }); + $menu = $('').appendTo('body').hide(); } else { $menu.empty(); } @@ -984,7 +1032,7 @@ ${propertyOptions}|show-property-type=true let url = mw.config.get('wgArticlePath').replace('$1', titleLabel); let liLink = document.createElement('li'); - liLink.classList.add('custom-menu-link-entry'); + liLink.classList.add('kg-node-properties-menu-link-entry'); liLink.innerHTML = '🔗 ' + titleLabel; liLink.addEventListener('click', () => window.open(url, '_blank')); $menu.append(liLink); @@ -993,26 +1041,59 @@ ${propertyOptions}|show-property-type=true fetchSemanticDataForNode(nodeId, function (rawProps) { let props = parseProperties(rawProps).filter(p => !p.property.startsWith('_')); nodePropertiesCache[title] = props; + let nodesExisting = Nodes.get(); + let edgesExisting = Edges.get(); if (props.length === 0) { $menu.append('