From 762bc7a54dc1aa409bea87726c43eef4b2af9114 Mon Sep 17 00:00:00 2001 From: Akriti Agrawal Date: Sun, 7 Jun 2026 23:57:44 +0530 Subject: [PATCH 1/3] Discover node and edge attributes from graph JSON --- static/js/attribute_mapping.js | 76 ++++++++++++++++++++++++++++++++++ templates/graph/index.html | 1 + 2 files changed, 77 insertions(+) create mode 100644 static/js/attribute_mapping.js diff --git a/static/js/attribute_mapping.js b/static/js/attribute_mapping.js new file mode 100644 index 00000000..2559bb5a --- /dev/null +++ b/static/js/attribute_mapping.js @@ -0,0 +1,76 @@ +/** + * Visual Attribute Mapping — attribute discovery from graph_json. + */ +var attributeMapping = { + RESERVED_NODE_ATTRS: [ 'id', 'name', 'label', 'aliases', 'popup', 'k', 'parent' ], + RESERVED_EDGE_ATTRS: [ 'id', 'source', 'target', 'name', 'is_directed', 'popup', 'k' ], + + discoveredAttributes: null, + + init: function () { + this.discoveredAttributes = this.extractAttributes( graph_json ); + }, + + extractAttributes: function ( graphJson ) { + if ( !graphJson || !graphJson.elements ) { + return { nodes: {}, edges: {} }; + } + + return { + nodes: this._scanElements( graphJson.elements.nodes, this.RESERVED_NODE_ATTRS ), + edges: this._scanElements( graphJson.elements.edges, this.RESERVED_EDGE_ATTRS ) + }; + }, + + _scanElements: function ( elements, reserved ) { + var attrs = {}; + var self = this; + + _.each( elements || [], function ( el ) { + _.each( el.data || {}, function ( value, key ) { + if ( reserved.indexOf( key ) !== -1 ) { + return; + } + if ( !attrs[ key ] ) { + attrs[ key ] = { values: [] }; + } + if ( value !== null && value !== undefined && value !== '' ) { + attrs[ key ].values.push( value ); + } + } ); + } ); + + var result = {}; + _.each( attrs, function ( info, key ) { + result[ key ] = self._classifyAttribute( info.values ); + } ); + return result; + }, + + _classifyAttribute: function ( values ) { + var unique = _.uniq( values ); + var allNumeric = unique.length > 0 && _.every( unique, function ( v ) { + return !isNaN( parseFloat( v ) ) && isFinite( v ); + } ); + + if ( allNumeric ) { + var nums = _.map( unique, parseFloat ); + return { + type: 'numerical', + min: _.min( nums ), + max: _.max( nums ), + count: values.length + }; + } + + return { + type: 'categorical', + values: unique.sort(), + count: values.length + }; + } +}; + +if ( typeof graph_json !== 'undefined' && graph_json ) { + attributeMapping.init(); +} diff --git a/templates/graph/index.html b/templates/graph/index.html index 674d010a..1cfed2ce 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -355,6 +355,7 @@

+ From 0dcbc99d4798542a1a7859089d41819b498adfca Mon Sep 17 00:00:00 2001 From: Akriti Agrawal Date: Mon, 15 Jun 2026 13:34:00 +0530 Subject: [PATCH 2/3] Add visual attribute mapping sidebar with open/close navigation from layout editor --- static/js/attribute_mapping.js | 28 ++++++++++ static/js/graphs_page.js | 3 ++ .../graph/attribute_mapping_sidebar.html | 53 +++++++++++++++++++ templates/graph/index.html | 1 + templates/graph/layout_editor_sidebar.html | 7 +++ 5 files changed, 92 insertions(+) create mode 100644 templates/graph/attribute_mapping_sidebar.html diff --git a/static/js/attribute_mapping.js b/static/js/attribute_mapping.js index 2559bb5a..a9248746 100644 --- a/static/js/attribute_mapping.js +++ b/static/js/attribute_mapping.js @@ -8,6 +8,10 @@ var attributeMapping = { discoveredAttributes: null, init: function () { + if ( typeof graph_json === 'undefined' || !graph_json ) { + this.discoveredAttributes = { nodes: {}, edges: {} }; + return; + } this.discoveredAttributes = this.extractAttributes( graph_json ); }, @@ -68,6 +72,30 @@ var attributeMapping = { values: unique.sort(), count: values.length }; + }, + + bindPanelEvents: function () { + $( '#mapAttributesBtn' ).off( 'click' ).on( 'click', function ( e ) { + e.preventDefault(); + attributeMapping.openPanel(); + } ); + + $( '#backToLayoutEditorBtn' ).off( 'click' ).on( 'click', function ( e ) { + e.preventDefault(); + attributeMapping.closePanel(); + } ); + }, + + openPanel: function () { + this.init(); + + $( '.gs-sidebar-nav' ).removeClass( 'active' ); + $( '#attributeMappingSideBar' ).addClass( 'active' ); + }, + + closePanel: function () { + $( '.gs-sidebar-nav' ).removeClass( 'active' ); + $( '#layoutEditorSideBar' ).addClass( 'active' ); } }; diff --git a/static/js/graphs_page.js b/static/js/graphs_page.js index e3c10393..72ba1a85 100644 --- a/static/js/graphs_page.js +++ b/static/js/graphs_page.js @@ -1451,6 +1451,9 @@ var graphPage = { cytoscapeGraph.contextMenu.init( graphPage.cyGraph ); + attributeMapping.init(); + attributeMapping.bindPanelEvents(); + graphPage.layoutEditor.undoRedoManager = new UndoManager( onUndo = function ( item ) { if ( item ) { diff --git a/templates/graph/attribute_mapping_sidebar.html b/templates/graph/attribute_mapping_sidebar.html new file mode 100644 index 00000000..5c1d77b0 --- /dev/null +++ b/templates/graph/attribute_mapping_sidebar.html @@ -0,0 +1,53 @@ + diff --git a/templates/graph/index.html b/templates/graph/index.html index 1cfed2ce..df48d00c 100644 --- a/templates/graph/index.html +++ b/templates/graph/index.html @@ -273,6 +273,7 @@

{% include 'graph/default_sidebar.html' %} {% if uid %} {% include 'graph/layout_editor_sidebar.html' %} + {% include 'graph/attribute_mapping_sidebar.html' %} {% include 'graph/legend/legend_editor_sidebar.html' %} {% endif %} {% include 'graph/filter_nodes_edges_sidebar.html' %} diff --git a/templates/graph/layout_editor_sidebar.html b/templates/graph/layout_editor_sidebar.html index dd4f4864..dcf7f847 100644 --- a/templates/graph/layout_editor_sidebar.html +++ b/templates/graph/layout_editor_sidebar.html @@ -59,6 +59,13 @@ +
  • + + Map Attributes + +
  • +
  • From d6889e006d9fe632fc34a9a801264a6766f407e5 Mon Sep 17 00:00:00 2001 From: Akriti Agrawal Date: Mon, 22 Jun 2026 12:53:32 +0530 Subject: [PATCH 3/3] Populate attribute and visual property dropdowns from discovered graph data --- static/js/attribute_mapping.js | 141 +++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/static/js/attribute_mapping.js b/static/js/attribute_mapping.js index a9248746..b426dab1 100644 --- a/static/js/attribute_mapping.js +++ b/static/js/attribute_mapping.js @@ -7,6 +7,20 @@ var attributeMapping = { discoveredAttributes: null, + // Which visual properties support which mapping types. + VISUAL_PROPERTIES: { + node: [ + { id: 'background-color', label: 'Node color', discrete: true, continuous: true }, + { id: 'width', label: 'Node size', discrete: false, continuous: true }, + { id: 'shape', label: 'Node shape', discrete: true, continuous: false } + ], + edge: [ + { id: 'line-color', label: 'Edge color', discrete: true, continuous: true }, + { id: 'width', label: 'Edge width', discrete: false, continuous: true }, + { id: 'line-style', label: 'Edge style', discrete: true, continuous: false } + ] + }, + init: function () { if ( typeof graph_json === 'undefined' || !graph_json ) { this.discoveredAttributes = { nodes: {}, edges: {} }; @@ -84,10 +98,137 @@ var attributeMapping = { e.preventDefault(); attributeMapping.closePanel(); } ); + + this.bindFormEvents(); + }, + + bindFormEvents: function () { + // When element type (nodes / edges) changes, reset to discrete mapping + // and repopulate both dropdowns based on the new element type. + $( '#mappingElementType' ).off( 'change' ).on( 'change', function () { + $( '#mappingMappingType' ).val( 'discrete' ); + attributeMapping.populateAttributeDropdown(); + attributeMapping.populateVisualPropertyDropdown(); + } ); + + // When attribute changes, infer mapping type (continuous vs discrete) + // from the discovered attribute meta and refresh visual properties. + $( '#mappingAttribute' ).off( 'change' ).on( 'change', function () { + attributeMapping.syncMappingTypeFromAttribute(); + attributeMapping.populateVisualPropertyDropdown(); + } ); + + // When mapping type is manually changed, just refresh visual properties. + $( '#mappingMappingType' ).off( 'change' ).on( 'change', function () { + attributeMapping.populateVisualPropertyDropdown(); + } ); + }, + + getSelectedElementType: function () { + var elementType = $( '#mappingElementType' ).val(); + return elementType === 'edge' ? 'edge' : 'node'; + }, + + getSelectedMappingType: function () { + return $( '#mappingMappingType' ).val() === 'continuous' ? 'continuous' : 'discrete'; + }, + + getVisualPropertiesForSelection: function () { + var elementType = this.getSelectedElementType(); + var mappingType = this.getSelectedMappingType(); + var properties = this.VISUAL_PROPERTIES[ elementType ] || []; + + return _.filter( properties, function ( property ) { + return mappingType === 'continuous' ? property.continuous : property.discrete; + } ); + }, + + getAttributesForElementType: function ( elementType ) { + var discovered = this.discoveredAttributes || { nodes: {}, edges: {} }; + return elementType === 'edge' ? discovered.edges : discovered.nodes; + }, + + getSelectedAttributeMeta: function () { + var attributeName = $( '#mappingAttribute' ).val(); + if ( !attributeName ) { + return null; + } + + var attributeMap = this.getAttributesForElementType( this.getSelectedElementType() ); + return attributeMap[ attributeName ] || null; + }, + + syncMappingTypeFromAttribute: function () { + var meta = this.getSelectedAttributeMeta(); + if ( !meta ) { + this.populateVisualPropertyDropdown(); + return; + } + + var mappingType = meta.type === 'numerical' ? 'continuous' : 'discrete'; + $( '#mappingMappingType' ).val( mappingType ); + this.populateVisualPropertyDropdown(); + }, + + populateAttributeDropdown: function () { + var elementType = this.getSelectedElementType(); + var attributeMap = this.getAttributesForElementType( elementType ); + var attributeNames = _.sortBy( _.keys( attributeMap ) ); + var $select = $( '#mappingAttribute' ); + + $select.empty(); + $select.append( $( '