# AGS Query Lens Visualization Best Practices

## Overview

This guide covers best practices for creating interactive, navigable visualizations using AGS Query Lenses. These patterns enable building sophisticated data exploration interfaces without complex JavaScript frameworks.

## Core Concepts

### Query Lens Structure

AGS Query Lenses consist of three main components:

1. **SPARQL Query** - Retrieves data from the knowledge graph
2. **HTML Template (EJS)** - Renders the visualization
3. **CSS Styles** - Provides visual styling

Additionally, **state variables** enable interactivity and navigation.

## State Management

### Defining State Variables

State variables enable interactive navigation and filtering:

```json
{
  "state": [
    {
      "name": "rootItem",
      "value": "DEFAULT_VALUE",
      "type": "string"
    },
    {
      "name": "path",
      "value": "DEFAULT_VALUE",
      "type": "string"
    }
  ]
}
```

### Accessing State in Queries

Use EJS template syntax to inject state into SPARQL:

```sparql
<% 
  const rootItem = state['rootItem'] || 'DEFAULT_VALUE';
%>

SELECT ?child ?childName ?childType
WHERE {
  ?parent :itemId '<%= rootItem %>' .
  ?child :parentRef ?parent ;
         :itemId ?childId .
  OPTIONAL { ?child :name ?childName }
  OPTIONAL { ?child :type ?childType }
}
```

## Interactive Navigation Pattern

### The Hidden Input + onclick Pattern

AGS automatically binds form elements with `name` attributes to state variables. The recommended pattern:

1. **Add hidden inputs** for each state variable
2. **Use onclick handlers** to update values and trigger refresh
3. **Dispatch change events** to notify AGS of state updates

### Implementation

```html
<!-- Hidden inputs bound to state -->
<input type="hidden" name="rootItem" value="<%=rootItem%>" />
<input type="hidden" name="path" value="<%=pathStr%>" />

<!-- Navigation button -->
<button onclick="var el=document.querySelector('[name=rootItem]');el.value='NEW_VALUE';el.dispatchEvent(new Event('change',{bubbles:true}))">
  Navigate
</button>
```

### Key Points

- Form elements with `name` attribute auto-bind to state
- Use `dispatchEvent(new Event('change', {bubbles:true}))` to trigger lens refresh
- AGS handles the refresh automatically - no need to call `lens.refresh()` explicitly

## Breadcrumb Navigation

### Path Tracking Strategy

Use a delimited string to track navigation history:

```javascript
// Store path as: "LEVEL1>LEVEL2>LEVEL3"
const pathStr = state['path'] || 'ROOT_VALUE';
const pathItems = pathStr.split('>');
```

### Navigation Helper Function

```javascript
function makeNavCode(itemId, pathStr) {
  const items = pathStr.split('>');
  const idx = items.indexOf(itemId);
  let newPath;
  
  // Backward navigation (item already in path)
  if (idx >= 0) {
    newPath = items.slice(0, idx + 1).join('>');
  } 
  // Forward navigation (new item)
  else {
    newPath = pathStr + '>' + itemId;
  }
  
  return "var e1=document.querySelector('[name=rootItem]');e1.value='" + itemId + 
         "';var e2=document.querySelector('[name=path]');e2.value='" + newPath + 
         "';e1.dispatchEvent(new Event('change',{bubbles:true}))";
}
```

### Rendering Breadcrumbs

```html
<div class="breadcrumb">
  <% pathItems.forEach((item, idx) => { %>
    <% if (idx > 0) { %><span class="separator">›</span><% } %>
    <% const isActive = idx === pathItems.length - 1; %>
    <span class="breadcrumb-item <%= isActive ? 'active' : '' %>" 
          <% if (!isActive) { %>
            onclick="<%=makeNavCode(item, pathStr)%>" 
            style="cursor:pointer"
          <% } %>>
      <% if (idx === 0) { %>🏠 <% } %><%=item%>
    </span>
  <% }); %>
</div>
```

## SPARQL Query Best Practices

### Keep Queries Simple

- **Avoid nested SELECT subqueries** - AGS query lenses work best with flat queries
- **Use OPTIONAL for nullable fields** - Prevents query from failing on missing data
- **Limit result sets** - Use `LIMIT` to prevent performance issues

### Pattern: Hierarchical Navigation Query

```sparql
PREFIX ex: <http://example.com/ontology#>

<% const currentNode = state['currentNode'] || 'ROOT'; %>

SELECT ?childNode ?childId ?childName ?childType ?quantity
WHERE {
  # Find the current node
  ?parent ex:nodeId '<%= currentNode %>' .
  
  # Find direct children
  ?childNode ex:parentNode ?parent ;
             ex:nodeId ?childId .
  
  # Get optional properties
  OPTIONAL { ?childNode ex:name ?childName }
  OPTIONAL { ?childNode ex:type ?childType }
  OPTIONAL { ?childNode ex:quantity ?quantity }
}
ORDER BY ?childName
LIMIT 100
```

## Data Processing in EJS

### Building Data Structures

```javascript
<%
  const items = [];
  const currentNode = state['currentNode'] || 'ROOT';
  
  bindings.forEach(b => {
    if (b.childId) {
      items.push({
        id: b.childId?.value,
        name: b.childName?.value || 'Unnamed',
        type: b.childType?.value || 'Unknown',
        quantity: b.quantity?.value || '1'
      });
    }
  });
%>
```

### Helper Functions for Display

```javascript
function getIcon(type) {
  if (type.includes('Category1')) return '🔵';
  if (type.includes('Category2')) return '🟢';
  return '⚪';
}

function getColor(type) {
  if (type.includes('Category1')) return '#1976D2';
  if (type.includes('Category2')) return '#00897B';
  return '#616161';
}
```

## CSS Styling Recommendations

### Modern Gradient Headers

```css
.header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 12px;
  padding: 24px 32px;
  box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
```

### Interactive Breadcrumbs

```css
.breadcrumb-item {
  color: #667eea;
  transition: all 0.2s;
  padding: 6px 12px;
  border-radius: 6px;
  font-weight: 500;
}

.breadcrumb-item:not(.active):hover {
  background: #f0f4ff;
  color: #5568d3;
  transform: translateY(-1px);
}

.breadcrumb-item.active {
  color: #2c3e50;
  background: #e6f2ff;
  font-weight: 600;
  cursor: default;
}
```

### Animated Table Rows

```css
@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.table-row {
  animation: slideIn 0.3s ease-out backwards;
}

.table-row:nth-child(1) { animation-delay: 0.05s; }
.table-row:nth-child(2) { animation-delay: 0.1s; }
.table-row:nth-child(3) { animation-delay: 0.15s; }
```

## Common Patterns

### Summary Cards

Display key metrics at a glance:

```html
<div class="summary-grid">
  <div class="card">
    <div class="card-icon">📦</div>
    <div class="card-content">
      <div class="card-value"><%=items.length%></div>
      <div class="card-label">Total Items</div>
    </div>
  </div>
  <div class="card">
    <div class="card-icon">📊</div>
    <div class="card-content">
      <div class="card-value"><%=pathItems.length%></div>
      <div class="card-label">Current Level</div>
    </div>
  </div>
</div>
```

### Empty State Messaging

```html
<% if (items.length === 0) { %>
  <div class="empty-state">
    <div class="empty-icon">📭</div>
    <h3>No Data Found</h3>
    <p>Try adjusting your filters or navigating to a different node.</p>
  </div>
<% } %>
```

### Export Functionality

AGS provides built-in export capability:

```html
<button class="btn exportButton" filename="data_export" format="csv">
  📥 Export CSV
</button>
```

## Troubleshooting

### Navigation Not Working

**Problem:** Clicking navigation buttons does nothing

**Solution:** Ensure you're:
1. Using hidden inputs with `name` attributes
2. Dispatching `change` events with `bubbles:true`
3. Updating the correct input element

### Query Syntax Errors

**Problem:** SPARQL query fails with nested SELECT errors

**Solution:**
- Remove nested `SELECT` subqueries
- Use flat query structure with `OPTIONAL` clauses
- Avoid `WITH` clauses in the main query body

### State Not Persisting

**Problem:** State resets after navigation

**Solution:**
- Verify hidden inputs have `name` attributes matching state variable names
- Ensure `value` attribute is set to current state: `value="<%=stateVar%>"`
- Check that navigation code updates both state variables

## Performance Considerations

1. **Limit Query Results:** Use `LIMIT` to cap result sets
2. **Index Properties:** Ensure frequently queried properties are indexed in AGS
3. **Minimize Data Processing:** Do complex calculations in SPARQL when possible
4. **Cache Static Data:** Use EJS variables for data that doesn't change per row

## Summary

The key to successful AGS Query Lens visualizations:

1. ✅ Use state variables for navigation and filtering
2. ✅ Bind state via hidden form inputs with `name` attributes
3. ✅ Update state with `dispatchEvent(new Event('change', {bubbles:true}))`
4. ✅ Keep SPARQL queries flat and simple
5. ✅ Implement breadcrumb navigation for multi-level exploration
6. ✅ Provide visual feedback with modern CSS and animations
7. ✅ Handle empty states gracefully

This pattern enables building sophisticated, interactive data exploration tools entirely within AGS Query Lenses, without requiring external web frameworks or complex JavaScript.
