diff --git a/README.md b/README.md index 24b3e7bb..0985f187 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,16 @@ # What is QLever UI? **QLever UI** is an easy-to-use interactive **user interface** for the **SPARQL search engine QLever** that helps to discover the scopes and information in very large knowledge bases by providing **context-sensitive suggestions** and **auto-completion** while adding helpful information and additional views to the various outputs of the queries. -QLever UI supports different types of results (e.g. geographical data, named instances, images, and more) and is highly customizable to the needs of its users and the structure of the underlying dataset. +QLever UI supports different types of results (e.g. geographical data, named instances, images, and more) and is highly customizable to the needs of its users and the structure of the underlying dataset. The interface includes **interactive query analysis** for exploring query execution and performance. + +## Key Features +- **Multi-backend support** - Configure and switch between different QLever instances +- **SPARQL formatting** - Automatic query formatting and syntax validation +- **Context-sensitive SPARQL autocompletion** - Smart suggestions based on query context +- **Customizable result views** - Support for geographic data, images, and structured results +- **Interactive query execution analysis** - Navigate through query trees to understand performance +- **Real-time query monitoring** - Live query progress updates during execution + ### What is QLever? QLever (pronounced "clever") is an efficient SPARQL engine that can handle very large datasets. For example, QLever can index the complete Wikidata (~ 18 billion triples) in less than 24 hours on a standard Linux machine using around 40 GB of RAM, with subsequent query times below 1 second even for relatively complex queries with large result sets. On top of the standard SPARQL functionality, QLever also supports SPARQL+Text search. @@ -45,6 +54,7 @@ Distributed under the Apache 2.0 License. See `LICENSE` for more information. * [Setting up the database manually](docs/install_qleverui.md#setting-up-the-database-manually) * [Running QLever UI without docker](docs/install_qleverui.md#running-qlever-ui-without-docker) * [Configure QLever UI](docs/configure_qleverui.md) +* [Query Analysis](docs/query-analysis.md) * [Extending QLever UI](#construct-and-theoretical-approach) * [Extending the language parser](docs/extending_parser.md) * [Extending the suggestions](docs/extending_suggestions.md) diff --git a/backend/static/css/style.css b/backend/static/css/style.css index 21b53489..9a90e9db 100644 --- a/backend/static/css/style.css +++ b/backend/static/css/style.css @@ -1,5 +1,5 @@ /* - + Basic HTML Elements */ @@ -13,7 +13,7 @@ kbd { } /* - + Custom Elements */ @@ -89,7 +89,7 @@ th { color: #82B36F; white-space: nowrap; } .d, #help, -#helpButton { +#helpButton { display: none; } @@ -99,7 +99,7 @@ th { color: #82B36F; white-space: nowrap; } #helpButton { display: inline; } - + #dynamicSuggestions { margin-top: 0px; } @@ -121,9 +121,9 @@ th { color: #82B36F; white-space: nowrap; } } /** - + CodeMirror Styles - + **/ .CodeMirror {max-width: 100%;} @@ -133,7 +133,7 @@ th { color: #82B36F; white-space: nowrap; } background: -moz-linear-gradient(top, #34759E 0%, #4787AD 100%); /* FF3.6-15 */ background: -webkit-linear-gradient(top, #34759E 0%,#4787AD 100%); /* Chrome10-25,Safari5.1-6 */ background: linear-gradient(to bottom, #34759E 0%,#4787AD 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ - border-radius: 5px; + border-radius: 5px; color: white; padding: 0px 5px 2px 5px; } @@ -199,9 +199,9 @@ th { color: #82B36F; white-space: nowrap; } .cm-s-railscasts .CodeMirror-activeline-background { background: #303040; } /** - + CodeMirror Hints - + **/ .CodeMirror-hint { @@ -212,7 +212,7 @@ th { color: #82B36F; white-space: nowrap; } color: black; cursor: pointer; position: relative; - line-height: 1.5; + line-height: 1.5; } .CodeMirror-hints { @@ -275,9 +275,62 @@ li.CodeMirror-hint-active { font-size: 80%; } +#result-query { + max-height: 200px; + overflow-y: auto; + margin-bottom: 10px; +} + /* required LIB STYLES */ /* .Treant se automatski dodaje na svaki chart conatiner */ .Treant { position: relative; overflow: hidden; padding: 0 !important; } +/* Visualization modal styles */ +#visualisation .modal-dialog { + width: 80%; +} + +#visualisation .modal-body { + height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +#visualisation .modal-header .pull-right { + margin-right: 30px; + font-size: 12px; + color: #666; +} + +/* Tree viewport and panzoom styles */ +#tree-viewport { + width: 100%; + flex: 1; + border: 1px solid #ddd; + overflow: auto; +} + +#result-tree { + position: relative; + cursor: grab; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +#result-tree .Treant { + overflow: visible; + width: auto !important; + height: auto !important; + min-width: 100%; + min-height: 100%; +} + +/* Query history styles */ +#lastQueries { + margin: -25px 0px; +} .Treant > .node, .Treant > .pseudo { position: absolute; display: block; visibility: hidden; } .Treant.Treant-loaded .node, diff --git a/backend/static/js/qleverUI.js b/backend/static/js/qleverUI.js index 869c9e94..30766340 100755 --- a/backend/static/js/qleverUI.js +++ b/backend/static/js/qleverUI.js @@ -84,6 +84,26 @@ $(document).ready(function () { entities.data('has-mouseenter-handler', 'true'); } + // Set up panzoom initialization and cleanup for visualization modal + $('#visualisation').on('shown.bs.modal', function () { + // Initialize panzoom when modal is displayed + if (typeof window.initializePanzoom === 'function') { + setTimeout(() => { + window.initializePanzoom(); + // Center the tree after initialization + setTimeout(() => { + centerTreeInViewport(); + }, 100); + }, 50); + } + }); + + $('#visualisation').on('hidden.bs.modal', function () { + if (typeof window.destroyPanzoom === 'function') { + window.destroyPanzoom(); + } + }); + // Initialization done. log('Editor initialized', 'other'); @@ -316,7 +336,7 @@ $(document).ready(function () { editor.getValue(), {"name_service": "if_checked"}); const queryRewrittenAndNormalizedAndWithEscapedQuotes = normalizeQuery(queryRewritten).replace(/\\/g, "\\\\").replace(/"/g, "\\\""); - + if (editor.state.completionActive) { editor.state.completionActive.close(); } // POST request to Django, for the query hash. @@ -389,7 +409,7 @@ $(document).ready(function () { accessToken.on("input", function () { updateBackendCommandVisibility(); }); - + }); function addNameHover(element, domElement, list, namepredicate, prefixes) { @@ -619,7 +639,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { try { let result = await fetchQleverBackend(params, headers); - + log('Evaluating and displaying results...', 'other'); if (result.status === "ERROR") { @@ -840,13 +860,13 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { } } } - + async function handleStatsDisplay() { try { log('Loading backend statistics...', 'other'); $('#statsButton span').html('Loading information...'); $('#statsButton').attr('disabled', 'disabled'); - + try { const response = await fetch(`${BASEURL}?cmd=stats`); if (!response.ok) { @@ -896,6 +916,47 @@ function showQueryPlanningTree(entry = undefined) { let currentTree = null; +// Centers and fits the tree in the viewport using panzoom +function centerTreeInViewport(attempt = 1, maxAttempts = 10) { + console.log('centerTreeInViewport called, attempt:', attempt); + const treeViewport = document.getElementById('tree-viewport'); + const resultTree = document.getElementById('result-tree'); + + if (!treeViewport || !resultTree) { + console.log('Missing elements: treeViewport=', treeViewport, 'resultTree=', resultTree); + return; + } + + // Look for any tree content - try multiple selectors + let treantContainer = resultTree.querySelector('.Treant') || + resultTree.querySelector('[class*="Treant"]') || + (resultTree.children.length > 0 ? resultTree : null); + + if (!treantContainer) { + console.log('No tree container found, attempt', attempt); + if (attempt < maxAttempts) { + setTimeout(() => centerTreeInViewport(attempt + 1, maxAttempts), 100); + } else { + console.log('Max attempts reached, giving up on centering'); + } + return; + } + + console.log('Found tree container, applying simple reset...'); + + // Simple approach - just reset panzoom to make tree visible + if (window.panzoomInstance) { + try { + window.panzoomInstance.reset(); + console.log('Tree reset to default view'); + } catch (error) { + console.log('Error during reset:', error.message); + } + } else { + console.log('No panzoom instance available'); + } +} + // Uses the information inside of request_log // to populate the DOM with the current runtime information. function renderRuntimeInformationToDom(entry = undefined) { @@ -954,6 +1015,8 @@ function renderRuntimeInformationToDom(entry = undefined) { container: "#result-tree", rootOrientation: "NORTH", connectors: { type: "step" }, + // Allow unlimited canvas size for large trees + scrollbar: "resize" }, nodeStructure: runtime_info["query_execution_tree"] } @@ -965,6 +1028,11 @@ function renderRuntimeInformationToDom(entry = undefined) { if (currentTree !== null) { currentTree.destroy(); } + // Destroy any existing panzoom instance + if (typeof window.destroyPanzoom === 'function') { + window.destroyPanzoom(); + } + currentTree = new Treant(treant_tree); $("#visualisation").scrollTop(scrollTop); $("#result-tree").scrollLeft(scrollLeft); diff --git a/backend/templates/partials/head.html b/backend/templates/partials/head.html index c97c06f7..70cf0737 100644 --- a/backend/templates/partials/head.html +++ b/backend/templates/partials/head.html @@ -79,6 +79,15 @@ window.determineOperationType = determineOperationType; + + + diff --git a/backend/templates/partials/modals/visualisation.html b/backend/templates/partials/modals/visualisation.html index 09efe6a8..68b292bb 100644 --- a/backend/templates/partials/modals/visualisation.html +++ b/backend/templates/partials/modals/visualisation.html @@ -1,24 +1,32 @@