From 4883cb21cb9141bf8f2b4e4a5098bf733efc30b4 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 19 Aug 2025 16:09:47 +0530 Subject: [PATCH 01/18] legends wip --- .../symbology/hooks/useGetSymbology.ts | 60 +++++++++++++ packages/base/src/mainview/Legends.tsx | 89 +++++++++++++++++++ packages/base/src/mainview/mainView.tsx | 11 +++ python/jupytergis_lab/style/base.css | 41 +++++++++ 4 files changed, 201 insertions(+) create mode 100644 packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts create mode 100644 packages/base/src/mainview/Legends.tsx diff --git a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts new file mode 100644 index 000000000..7d4435eef --- /dev/null +++ b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts @@ -0,0 +1,60 @@ +import { IJupyterGISModel } from '@jupytergis/schema'; +import { useEffect, useState } from 'react'; + +interface IUseGetSymbologyProps { + layerId?: string; + model: IJupyterGISModel; +} + +interface IUseGetSymbologyResult { + symbology: Record | null; + isLoading: boolean; + error?: Error; +} + +/** + * Extracts symbology information (paint/layout + symbologyState) + * for a given layer from the JupyterGIS model. + */ +export const useGetSymbology = ({ + layerId, + model, +}: IUseGetSymbologyProps): IUseGetSymbologyResult => { + const [symbology, setSymbology] = useState | null>(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + useEffect(() => { + if (!layerId) {return;} + + try { + setIsLoading(true); + setError(undefined); + + const layer = model.getLayer(layerId); + + if (!layer) { + throw new Error(`Layer not found: ${layerId}`); + } + + const params = layer.parameters ?? {}; + const { symbologyState, color, ...rest } = params; + + // Merge both style props + high-level symbology metadata + const result: Record = { + ...rest, + ...(color ? { color } : {}), + ...(symbologyState ? { symbologyState } : {}), + }; + + setSymbology(result); + } catch (err) { + setError(err as Error); + setSymbology(null); + } finally { + setIsLoading(false); + } + }, [layerId, model]); + + return { symbology, isLoading, error }; +}; diff --git a/packages/base/src/mainview/Legends.tsx b/packages/base/src/mainview/Legends.tsx new file mode 100644 index 000000000..b81a5e619 --- /dev/null +++ b/packages/base/src/mainview/Legends.tsx @@ -0,0 +1,89 @@ +import { IJupyterGISModel } from '@jupytergis/schema'; +import React, { useState } from 'react'; +import Draggable from 'react-draggable'; + +import { useGetSymbology } from '../dialogs/symbology/hooks/useGetSymbology'; + +interface ILegendsProps { + layerId: string; + model: IJupyterGISModel; +} + +const Legends: React.FC = ({ layerId, model }) => { + const [collapsed, setCollapsed] = useState(false); + + const { symbology, isLoading, error } = useGetSymbology({ layerId, model }); + console.log('symbology', symbology); + + const parseColorStops = (fillColor: any): { value: number; color: string }[] => { + if (!Array.isArray(fillColor) || fillColor[0] !== 'interpolate') { + return []; + } + + const stops: { value: number; color: string }[] = []; + for (let i = 3; i < fillColor.length; i += 2) { + const value = fillColor[i] as number; + const rgba = fillColor[i + 1] as [number, number, number, number]; + const color = `rgba(${rgba[0]},${rgba[1]},${rgba[2]},${rgba[3]})`; + stops.push({ value, color }); + } + return stops; + }; + + const stops = parseColorStops(symbology?.color?.['fill-color']); + + return ( + +
+ {/* Header */} +
+ Legends + +
+ + {/* Body */} + {!collapsed && ( +
+ {isLoading &&

Loading...

} + {error &&

{error.message}

} + + {!isLoading && symbology ? ( + <> + {stops.length > 0 ? ( + stops.map((stop, idx) => ( +
+ + {stop.value.toFixed(2)} +
+ )) + ) : ( +

No symbology available

+ )} + + ) : null} +
+ )} +
+
+ ); +}; + +export default Legends; diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 82caddef6..82eb2a4be 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -102,6 +102,7 @@ import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; import { LeftPanel, RightPanel } from '../panelview'; +import Legends from './Legends'; type OlLayerTypes = | TileLayer @@ -2299,6 +2300,16 @@ export class MainView extends React.Component { /> + + + {this._state && ( div.jp-Toolbar-item:last-child { .jp-toolbar-users-item > .lm-MenuBar-itemIcon.selected { border-color: var(--jp-brand-color1); } + +.legends-container { + width: 250px; + height: 200px; + background: white; + border: 1px solid #ccc; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + overflow: hidden; + z-index: 1000; +} + +.legends-container.collapsed { + width: 200px; + height: 40px; +} + +.legends-header { + background: #f4f4f4; + padding: 6px; + cursor: move; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #ddd; +} + +.legends-title { + font-weight: bold; +} + +.legends-toggle { + border: none; + background: transparent; + cursor: pointer; + font-size: 14px; +} + +.legends-body { + padding: 10px; +} From 85056a1f78b2772536dd5ad47240336bafa7f8e7 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 19 Aug 2025 17:03:18 +0530 Subject: [PATCH 02/18] lint --- .../dialogs/symbology/hooks/useGetSymbology.ts | 4 +++- packages/base/src/mainview/mainView.tsx | 16 ++++++++-------- python/jupytergis_lab/style/base.css | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts index 7d4435eef..e999367b2 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts @@ -25,7 +25,9 @@ export const useGetSymbology = ({ const [error, setError] = useState(); useEffect(() => { - if (!layerId) {return;} + if (!layerId) { + return; + } try { setIsLoading(true); diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 82eb2a4be..5b053ce03 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -2301,14 +2301,14 @@ export class MainView extends React.Component { - + model={this._model} + layerId={ + Object.keys( + this._model?.sharedModel.awareness.getLocalState()?.selected + ?.value || {}, + )[0] + } + /> {this._state && ( div.jp-Toolbar-item:last-child { background: white; border: 1px solid #ccc; border-radius: 6px; - box-shadow: 0 2px 6px rgba(0,0,0,0.2); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); overflow: hidden; z-index: 1000; } From 29415a4d01c356c32173a147186d5591bf3a4623 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 19 Aug 2025 17:03:31 +0530 Subject: [PATCH 03/18] categorised working --- packages/base/src/mainview/Legends.tsx | 211 ++++++++++++++++++++----- 1 file changed, 173 insertions(+), 38 deletions(-) diff --git a/packages/base/src/mainview/Legends.tsx b/packages/base/src/mainview/Legends.tsx index b81a5e619..ff01b9f92 100644 --- a/packages/base/src/mainview/Legends.tsx +++ b/packages/base/src/mainview/Legends.tsx @@ -11,35 +11,194 @@ interface ILegendsProps { const Legends: React.FC = ({ layerId, model }) => { const [collapsed, setCollapsed] = useState(false); - const { symbology, isLoading, error } = useGetSymbology({ layerId, model }); console.log('symbology', symbology); - const parseColorStops = (fillColor: any): { value: number; color: string }[] => { - if (!Array.isArray(fillColor) || fillColor[0] !== 'interpolate') { - return []; - } - + // ๐Ÿ”น Parse interpolate expression into discrete stops + const parseColorStops = (expr: any): { value: number; color: string }[] => { + if (!Array.isArray(expr) || expr[0] !== 'interpolate') {return [];} const stops: { value: number; color: string }[] = []; - for (let i = 3; i < fillColor.length; i += 2) { - const value = fillColor[i] as number; - const rgba = fillColor[i + 1] as [number, number, number, number]; - const color = `rgba(${rgba[0]},${rgba[1]},${rgba[2]},${rgba[3]})`; + for (let i = 3; i < expr.length; i += 2) { + const value = expr[i] as number; + const rgba = expr[i + 1] as [number, number, number, number]; + const color = Array.isArray(rgba) + ? `rgba(${rgba[0]},${rgba[1]},${rgba[2]},${rgba[3]})` + : String(rgba); stops.push({ value, color }); } return stops; }; - const stops = parseColorStops(symbology?.color?.['fill-color']); + // ๐Ÿ”น Parse "case" expression into categories + const parseCaseCategories = ( + expr: any, + ): { category: string | number; color: string }[] => { + if (!Array.isArray(expr) || expr[0] !== 'case') {return [];} + + const categories: { category: string | number; color: string }[] = []; + + // Loop over conditions in pairs: condition, color + for (let i = 1; i < expr.length - 1; i += 2) { + const condition = expr[i]; + const colorExpr = expr[i + 1]; + + // condition is usually ["==", ["get", "field"], value] + let category; + if ( + Array.isArray(condition) && + condition[0] === '==' && + Array.isArray(condition[2]) + ) { + category = condition[2]; + } else if (Array.isArray(condition) && condition[0] === '==') { + category = condition[2]; + } + + // colorExpr is an array [r,g,b,a] + let color = ''; + if (Array.isArray(colorExpr)) { + color = `rgba(${colorExpr[0]},${colorExpr[1]},${colorExpr[2]},${colorExpr[3]})`; + } else if (typeof colorExpr === 'string') { + color = colorExpr; + } + + categories.push({ category, color }); + } + + return categories; + }; + + const renderLegend = () => { + if (!symbology) {return

No symbology available

;} + + const state = symbology.symbologyState?.renderType; + + // ๐Ÿ”น Single Symbol + if (state === 'Single Symbol') { + const color = + symbology.color?.['fill-color'] || symbology.color?.['stroke-color']; + return ( +
+ + Layer +
+ ); + } + + // ๐Ÿ”น Graduated (discrete steps from interpolate) + if (state === 'Graduated') { + const stops = parseColorStops( + symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], + ); + if (stops.length === 0) {return

No graduated symbology

;} + + return ( + <> + {stops.map((stop, idx) => ( +
+ + {stop.value.toFixed(2)} +
+ ))} + + ); + } + + // ๐Ÿ”น Categorized (continuous ramp with ticks) + // ๐Ÿ”น Categorized (case expression) + if (state === 'Categorized') { + const categories = parseCaseCategories( + symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], + ); + if (categories.length === 0) {return

No categorized symbology

;} + + return ( +
+ {categories.map((c, idx) => ( +
+ + {String(c.category)} +
+ ))} +
+ ); + } + + return

Unsupported render type: {state}

; + }; return (
{/* Header */} -
+
Legends
From c04b2e88bf094b9318635ac19b688babc71bfa8e Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 19 Aug 2025 17:30:08 +0530 Subject: [PATCH 04/18] style fix --- packages/base/src/mainview/Legends.tsx | 11 ++--------- python/jupytergis_lab/style/base.css | 2 ++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/base/src/mainview/Legends.tsx b/packages/base/src/mainview/Legends.tsx index ff01b9f92..ddec8c192 100644 --- a/packages/base/src/mainview/Legends.tsx +++ b/packages/base/src/mainview/Legends.tsx @@ -14,7 +14,6 @@ const Legends: React.FC = ({ layerId, model }) => { const { symbology, isLoading, error } = useGetSymbology({ layerId, model }); console.log('symbology', symbology); - // ๐Ÿ”น Parse interpolate expression into discrete stops const parseColorStops = (expr: any): { value: number; color: string }[] => { if (!Array.isArray(expr) || expr[0] !== 'interpolate') {return [];} const stops: { value: number; color: string }[] = []; @@ -29,7 +28,6 @@ const Legends: React.FC = ({ layerId, model }) => { return stops; }; - // ๐Ÿ”น Parse "case" expression into categories const parseCaseCategories = ( expr: any, ): { category: string | number; color: string }[] => { @@ -37,12 +35,11 @@ const Legends: React.FC = ({ layerId, model }) => { const categories: { category: string | number; color: string }[] = []; - // Loop over conditions in pairs: condition, color for (let i = 1; i < expr.length - 1; i += 2) { const condition = expr[i]; const colorExpr = expr[i + 1]; - // condition is usually ["==", ["get", "field"], value] + let category; if ( Array.isArray(condition) && @@ -54,7 +51,7 @@ const Legends: React.FC = ({ layerId, model }) => { category = condition[2]; } - // colorExpr is an array [r,g,b,a] + let color = ''; if (Array.isArray(colorExpr)) { color = `rgba(${colorExpr[0]},${colorExpr[1]},${colorExpr[2]},${colorExpr[3]})`; @@ -73,7 +70,6 @@ const Legends: React.FC = ({ layerId, model }) => { const state = symbology.symbologyState?.renderType; - // ๐Ÿ”น Single Symbol if (state === 'Single Symbol') { const color = symbology.color?.['fill-color'] || symbology.color?.['stroke-color']; @@ -98,7 +94,6 @@ const Legends: React.FC = ({ layerId, model }) => { ); } - // ๐Ÿ”น Graduated (discrete steps from interpolate) if (state === 'Graduated') { const stops = parseColorStops( symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], @@ -135,8 +130,6 @@ const Legends: React.FC = ({ layerId, model }) => { ); } - // ๐Ÿ”น Categorized (continuous ramp with ticks) - // ๐Ÿ”น Categorized (case expression) if (state === 'Categorized') { const categories = parseCaseCategories( symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], diff --git a/python/jupytergis_lab/style/base.css b/python/jupytergis_lab/style/base.css index 102370fab..136cb5fb3 100644 --- a/python/jupytergis_lab/style/base.css +++ b/python/jupytergis_lab/style/base.css @@ -880,4 +880,6 @@ div.jGIS-toolbar-widget > div.jp-Toolbar-item:last-child { .legends-body { padding: 10px; + max-height: 150px; + overflow: auto; } From 51209f9ad8a44607ba941a2933070b57e0f625b9 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Wed, 20 Aug 2025 13:05:43 +0530 Subject: [PATCH 05/18] don't fire too often --- .../symbology/hooks/useGetSymbology.ts | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts index e999367b2..7e31b4fe4 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts @@ -15,6 +15,7 @@ interface IUseGetSymbologyResult { /** * Extracts symbology information (paint/layout + symbologyState) * for a given layer from the JupyterGIS model. + * Keeps symbology updated when the layer changes. */ export const useGetSymbology = ({ layerId, @@ -29,33 +30,61 @@ export const useGetSymbology = ({ return; } - try { - setIsLoading(true); - setError(undefined); + let disposed = false; - const layer = model.getLayer(layerId); + const fetchSymbology = () => { + try { + setIsLoading(true); + setError(undefined); - if (!layer) { - throw new Error(`Layer not found: ${layerId}`); + const layer = model.getLayer(layerId); + + if (!layer) { + throw new Error(`Layer not found: ${layerId}`); + } + + const params = layer.parameters ?? {}; + const { symbologyState, color, ...rest } = params; + + const result: Record = { + ...rest, + ...(color ? { color } : {}), + ...(symbologyState ? { symbologyState } : {}), + }; + + if (!disposed) { + setSymbology(result); + } + } catch (err) { + if (!disposed) { + setError(err as Error); + setSymbology(null); + } + } finally { + if (!disposed) { + setIsLoading(false); + } } + }; - const params = layer.parameters ?? {}; - const { symbologyState, color, ...rest } = params; - - // Merge both style props + high-level symbology metadata - const result: Record = { - ...rest, - ...(color ? { color } : {}), - ...(symbologyState ? { symbologyState } : {}), - }; - - setSymbology(result); - } catch (err) { - setError(err as Error); - setSymbology(null); - } finally { - setIsLoading(false); - } + // initial load + fetchSymbology(); + + + model.sharedLayersChanged.connect(() => { + if (model.getLayer(layerId)) { + fetchSymbology(); + } else { + if (!disposed) { + setSymbology(null); + setIsLoading(false); + } + } + }); + + return () => { + disposed = true; + }; }, [layerId, model]); return { symbology, isLoading, error }; From 121fcd370f4c9b0ea3063bc5acfed5ce17237ddf Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Wed, 20 Aug 2025 17:18:11 +0530 Subject: [PATCH 06/18] do something --- .../symbology/hooks/useGetSymbology.ts | 6 ++++- packages/base/src/mainview/Legends.tsx | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts index 7e31b4fe4..21410187a 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts @@ -70,7 +70,6 @@ export const useGetSymbology = ({ // initial load fetchSymbology(); - model.sharedLayersChanged.connect(() => { if (model.getLayer(layerId)) { fetchSymbology(); @@ -82,6 +81,11 @@ export const useGetSymbology = ({ } }); + model.sharedModel.awareness.on('change', () => { + console.log(`Awareness changed for layer ${layerId}`); + fetchSymbology(); + }); + return () => { disposed = true; }; diff --git a/packages/base/src/mainview/Legends.tsx b/packages/base/src/mainview/Legends.tsx index ddec8c192..623d59f30 100644 --- a/packages/base/src/mainview/Legends.tsx +++ b/packages/base/src/mainview/Legends.tsx @@ -15,7 +15,9 @@ const Legends: React.FC = ({ layerId, model }) => { console.log('symbology', symbology); const parseColorStops = (expr: any): { value: number; color: string }[] => { - if (!Array.isArray(expr) || expr[0] !== 'interpolate') {return [];} + if (!Array.isArray(expr) || expr[0] !== 'interpolate') { + return []; + } const stops: { value: number; color: string }[] = []; for (let i = 3; i < expr.length; i += 2) { const value = expr[i] as number; @@ -31,7 +33,9 @@ const Legends: React.FC = ({ layerId, model }) => { const parseCaseCategories = ( expr: any, ): { category: string | number; color: string }[] => { - if (!Array.isArray(expr) || expr[0] !== 'case') {return [];} + if (!Array.isArray(expr) || expr[0] !== 'case') { + return []; + } const categories: { category: string | number; color: string }[] = []; @@ -39,7 +43,6 @@ const Legends: React.FC = ({ layerId, model }) => { const condition = expr[i]; const colorExpr = expr[i + 1]; - let category; if ( Array.isArray(condition) && @@ -51,7 +54,6 @@ const Legends: React.FC = ({ layerId, model }) => { category = condition[2]; } - let color = ''; if (Array.isArray(colorExpr)) { color = `rgba(${colorExpr[0]},${colorExpr[1]},${colorExpr[2]},${colorExpr[3]})`; @@ -66,7 +68,9 @@ const Legends: React.FC = ({ layerId, model }) => { }; const renderLegend = () => { - if (!symbology) {return

No symbology available

;} + if (!symbology) { + return

No symbology available

; + } const state = symbology.symbologyState?.renderType; @@ -98,7 +102,9 @@ const Legends: React.FC = ({ layerId, model }) => { const stops = parseColorStops( symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], ); - if (stops.length === 0) {return

No graduated symbology

;} + if (stops.length === 0) { + return

No graduated symbology

; + } return ( <> @@ -134,7 +140,9 @@ const Legends: React.FC = ({ layerId, model }) => { const categories = parseCaseCategories( symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], ); - if (categories.length === 0) {return

No categorized symbology

;} + if (categories.length === 0) { + return

No categorized symbology

; + } return (
From faed8b207ad4e0a56890b4b829ebcdb983a894d5 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Sat, 23 Aug 2025 21:19:47 +0530 Subject: [PATCH 07/18] rework --- .../symbology/hooks/useGetSymbology.ts | 4 +- packages/base/src/mainview/mainView.tsx | 6 +- .../base/src/panelview/components/layers.tsx | 32 ++- .../src/panelview/components/legendItem.tsx | 240 ++++++++++++++++++ 4 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 packages/base/src/panelview/components/legendItem.tsx diff --git a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts index 21410187a..3ef6c9e71 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts @@ -82,8 +82,8 @@ export const useGetSymbology = ({ }); model.sharedModel.awareness.on('change', () => { - console.log(`Awareness changed for layer ${layerId}`); - fetchSymbology(); + console.log(`Awareness changed for layer ${layerId}`); + fetchSymbology(); }); return () => { diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 5b053ce03..1a7c777cb 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -102,7 +102,7 @@ import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; import { LeftPanel, RightPanel } from '../panelview'; -import Legends from './Legends'; +// import Legends from './Legends'; type OlLayerTypes = | TileLayer @@ -2300,7 +2300,7 @@ export class MainView extends React.Component { />
- { ?.value || {}, )[0] } - /> + /> */} {this._state && ( = props => { // TODO Support multi-selection as `model?.jGISModel?.localState?.selected.value` does isSelected(layerId, gisModel), ); + const [expanded, setExpanded] = useState(false); + const name = layer.name; useEffect(() => { @@ -450,6 +458,22 @@ const LayerComponent: React.FC = props => { onClick={setSelection} onContextMenu={setSelection} > + {/* Expand/collapse legend */} + + + {/* Visibility toggle */}
+ + {expanded && gisModel && ( +
+ +
+ )}
); }; diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx new file mode 100644 index 000000000..d7dd4673a --- /dev/null +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -0,0 +1,240 @@ +import { IJupyterGISModel } from '@jupytergis/schema'; +import React, { useEffect, useState } from 'react'; + +import { useGetSymbology } from '@/src/dialogs/symbology/hooks/useGetSymbology'; + +export const LegendItem: React.FC<{ + layerId: string; + model: IJupyterGISModel; +}> = ({ layerId, model }) => { + const { symbology, isLoading, error } = useGetSymbology({ layerId, model }); + const [content, setContent] = useState(null); + + const parseColorStops = (expr: any): { value: number; color: string }[] => { + if (!Array.isArray(expr) || expr[0] !== 'interpolate') {return [];} + const stops: { value: number; color: string }[] = []; + for (let i = 3; i < expr.length; i += 2) { + const value = expr[i] as number; + const rgba = expr[i + 1] as [number, number, number, number] | string; + const color = Array.isArray(rgba) + ? `rgba(${rgba[0]},${rgba[1]},${rgba[2]},${rgba[3]})` + : String(rgba); + stops.push({ value, color }); + } + return stops; + }; + + const parseCaseCategories = ( + expr: any, + ): { category: string | number; color: string }[] => { + if (!Array.isArray(expr) || expr[0] !== 'case') {return [];} + const categories: { category: string | number; color: string }[] = []; + for (let i = 1; i < expr.length - 1; i += 2) { + const condition = expr[i]; + const colorExpr = expr[i + 1]; + let category: any = ''; + if (Array.isArray(condition) && condition[0] === '==') { + category = condition[2]; + } + let color = ''; + if (Array.isArray(colorExpr)) { + color = `rgba(${colorExpr[0]},${colorExpr[1]},${colorExpr[2]},${colorExpr[3]})`; + } else if (typeof colorExpr === 'string') { + color = colorExpr; + } + categories.push({ category, color }); + } + return categories; + }; + + useEffect(() => { + if (isLoading) { + setContent(

Loadingโ€ฆ

); + return; + } + if (error) { + setContent( +

{error.message}

, + ); + return; + } + if (!symbology) { + setContent(

No symbology

); + return; + } + + const renderType = symbology.symbologyState?.renderType; + const fill = + symbology.color?.['fill-color'] ?? symbology.color?.['circle-fill-color']; + const stroke = + symbology.color?.['stroke-color'] ?? + symbology.color?.['circle-stroke-color']; + + // Single Symbol + if (renderType === 'Single Symbol') { + setContent( +
+ {fill && ( +
+ + Fill +
+ )} + {stroke && ( +
+ + Stroke +
+ )} + {!fill && !stroke && ( + No symbol colors + )} +
, + ); + return; + } + + // Graduated + if (renderType === 'Graduated') { + const stops = parseColorStops(fill || stroke); + if (!stops.length) { + setContent(

No graduated symbology

); + return; + } + const values = stops.map(s => s.value); + const colors = stops.map(s => s.color); + const ranges: string[] = []; + for (let i = 0; i < values.length; i++) { + if (i === 0) {ranges.push(`< ${values[0].toFixed(2)}`);} + else if (i === values.length - 1) + {ranges.push(`> ${values[values.length - 1].toFixed(2)}`);} + else + {ranges.push(`${values[i - 1].toFixed(2)} โ€“ ${values[i].toFixed(2)}`);} + } + setContent( +
+ {ranges.map((label, i) => ( +
+ + {label} +
+ ))} +
, + ); + return; + } + + // Categorized + if (renderType === 'Categorized') { + const cats = parseCaseCategories(fill || stroke); + if (!cats.length) { + setContent( +

No categorized symbology

, + ); + return; + } + + const n = cats.length; + const tickCount = 5; + const tickIdx = Array.from({ length: tickCount }, (_, i) => + Math.round((i * (n - 1)) / (tickCount - 1)), + ); + const uniqueIdx = Array.from(new Set(tickIdx)); + const labeled = uniqueIdx.map(i => cats[i]); + + const segments = cats + .map((c, i) => { + const start = (i * 100) / n; + const end = ((i + 1) * 100) / n; + return `${c.color} ${start}% ${end}%`; + }) + .join(', '); + const gradient = `linear-gradient(to top, ${segments})`; + + setContent( +
+
+
+ {Array.from({ length: tickCount }, (_, i) => { + const pos = (i * 100) / (tickCount - 1); + return ( +
+
+
+ ); + })} +
+
+ {labeled.map((c, i) => ( + + {String(c.category)} + + ))} +
+
, + ); + return; + } + + setContent( +

+ Unsupported symbology: {String(renderType)} +

, + ); + }, [symbology, isLoading, error]); + + return ( +
+ {content} +
+ ); +}; From 2246863a3c3dcd6119f806d276191f36a02ca0df Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Sat, 23 Aug 2025 21:43:39 +0530 Subject: [PATCH 08/18] style fix --- packages/base/src/panelview/components/layers.tsx | 4 +++- packages/base/style/leftPanel.css | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/base/src/panelview/components/layers.tsx b/packages/base/src/panelview/components/layers.tsx index 7d65dc059..0494f17c5 100644 --- a/packages/base/src/panelview/components/layers.tsx +++ b/packages/base/src/panelview/components/layers.tsx @@ -452,11 +452,13 @@ const LayerComponent: React.FC = props => { onDragOver={Private.onDragOver} onDragEnd={Private.onDragEnd} data-id={layerId} + style={{ display: 'flex', flexDirection: 'column' }} >
{/* Expand/collapse legend */}
{expanded && gisModel && ( -
+
)} diff --git a/packages/base/style/leftPanel.css b/packages/base/style/leftPanel.css index 4a5d4c5fe..6765a7ec6 100644 --- a/packages/base/style/leftPanel.css +++ b/packages/base/style/leftPanel.css @@ -41,7 +41,7 @@ .jp-gis-source { display: flex; flex-direction: row; - align-items: center; + /* align-items: center; */ color: var(--jp-ui-font-color1); } From b9551847b5603d33b4c7ef61a24b1645a2fd88d5 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Sun, 24 Aug 2025 17:41:07 +0530 Subject: [PATCH 09/18] horizontal colorbar --- .../src/panelview/components/legendItem.tsx | 93 ++++++++++--------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx index d7dd4673a..7918610e6 100644 --- a/packages/base/src/panelview/components/legendItem.tsx +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -11,7 +11,9 @@ export const LegendItem: React.FC<{ const [content, setContent] = useState(null); const parseColorStops = (expr: any): { value: number; color: string }[] => { - if (!Array.isArray(expr) || expr[0] !== 'interpolate') {return [];} + if (!Array.isArray(expr) || expr[0] !== 'interpolate') { + return []; + } const stops: { value: number; color: string }[] = []; for (let i = 3; i < expr.length; i += 2) { const value = expr[i] as number; @@ -27,7 +29,9 @@ export const LegendItem: React.FC<{ const parseCaseCategories = ( expr: any, ): { category: string | number; color: string }[] => { - if (!Array.isArray(expr) || expr[0] !== 'case') {return [];} + if (!Array.isArray(expr) || expr[0] !== 'case') { + return []; + } const categories: { category: string | number; color: string }[] = []; for (let i = 1; i < expr.length - 1; i += 2) { const condition = expr[i]; @@ -119,11 +123,13 @@ export const LegendItem: React.FC<{ const colors = stops.map(s => s.color); const ranges: string[] = []; for (let i = 0; i < values.length; i++) { - if (i === 0) {ranges.push(`< ${values[0].toFixed(2)}`);} - else if (i === values.length - 1) - {ranges.push(`> ${values[values.length - 1].toFixed(2)}`);} - else - {ranges.push(`${values[i - 1].toFixed(2)} โ€“ ${values[i].toFixed(2)}`);} + if (i === 0) { + ranges.push(`< ${values[0].toFixed(2)}`); + } else if (i === values.length - 1) { + ranges.push(`> ${values[values.length - 1].toFixed(2)}`); + } else { + ranges.push(`${values[i - 1].toFixed(2)} โ€“ ${values[i].toFixed(2)}`); + } } setContent(
@@ -160,12 +166,12 @@ export const LegendItem: React.FC<{ } const n = cats.length; - const tickCount = 5; + const tickCount = Math.min(6, n); // limit ticks (e.g. 6 max) const tickIdx = Array.from({ length: tickCount }, (_, i) => Math.round((i * (n - 1)) / (tickCount - 1)), ); const uniqueIdx = Array.from(new Set(tickIdx)); - const labeled = uniqueIdx.map(i => cats[i]); + const labeled = uniqueIdx.map(i => ({ ...cats[i], idx: i })); const segments = cats .map((c, i) => { @@ -174,52 +180,55 @@ export const LegendItem: React.FC<{ return `${c.color} ${start}% ${end}%`; }) .join(', '); - const gradient = `linear-gradient(to top, ${segments})`; + const gradient = `linear-gradient(to right, ${segments})`; setContent( -
-
-
- {Array.from({ length: tickCount }, (_, i) => { - const pos = (i * 100) / (tickCount - 1); +
+
+ {labeled.map((c, i) => { + const left = (c.idx * 100) / (n - 1); + const up = i % 2 === 0; return (
-
+
+
+ {String(c.category)} +
); })}
-
- {labeled.map((c, i) => ( - - {String(c.category)} - - ))} -
, ); return; From 641428e327aabef9cfb1e45dec3b655a8de4a7b2 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Sun, 24 Aug 2025 17:52:33 +0530 Subject: [PATCH 10/18] not needed --- packages/base/src/mainview/Legends.tsx | 225 ------------------------ packages/base/src/mainview/mainView.tsx | 11 -- 2 files changed, 236 deletions(-) delete mode 100644 packages/base/src/mainview/Legends.tsx diff --git a/packages/base/src/mainview/Legends.tsx b/packages/base/src/mainview/Legends.tsx deleted file mode 100644 index 623d59f30..000000000 --- a/packages/base/src/mainview/Legends.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { IJupyterGISModel } from '@jupytergis/schema'; -import React, { useState } from 'react'; -import Draggable from 'react-draggable'; - -import { useGetSymbology } from '../dialogs/symbology/hooks/useGetSymbology'; - -interface ILegendsProps { - layerId: string; - model: IJupyterGISModel; -} - -const Legends: React.FC = ({ layerId, model }) => { - const [collapsed, setCollapsed] = useState(false); - const { symbology, isLoading, error } = useGetSymbology({ layerId, model }); - console.log('symbology', symbology); - - const parseColorStops = (expr: any): { value: number; color: string }[] => { - if (!Array.isArray(expr) || expr[0] !== 'interpolate') { - return []; - } - const stops: { value: number; color: string }[] = []; - for (let i = 3; i < expr.length; i += 2) { - const value = expr[i] as number; - const rgba = expr[i + 1] as [number, number, number, number]; - const color = Array.isArray(rgba) - ? `rgba(${rgba[0]},${rgba[1]},${rgba[2]},${rgba[3]})` - : String(rgba); - stops.push({ value, color }); - } - return stops; - }; - - const parseCaseCategories = ( - expr: any, - ): { category: string | number; color: string }[] => { - if (!Array.isArray(expr) || expr[0] !== 'case') { - return []; - } - - const categories: { category: string | number; color: string }[] = []; - - for (let i = 1; i < expr.length - 1; i += 2) { - const condition = expr[i]; - const colorExpr = expr[i + 1]; - - let category; - if ( - Array.isArray(condition) && - condition[0] === '==' && - Array.isArray(condition[2]) - ) { - category = condition[2]; - } else if (Array.isArray(condition) && condition[0] === '==') { - category = condition[2]; - } - - let color = ''; - if (Array.isArray(colorExpr)) { - color = `rgba(${colorExpr[0]},${colorExpr[1]},${colorExpr[2]},${colorExpr[3]})`; - } else if (typeof colorExpr === 'string') { - color = colorExpr; - } - - categories.push({ category, color }); - } - - return categories; - }; - - const renderLegend = () => { - if (!symbology) { - return

No symbology available

; - } - - const state = symbology.symbologyState?.renderType; - - if (state === 'Single Symbol') { - const color = - symbology.color?.['fill-color'] || symbology.color?.['stroke-color']; - return ( -
- - Layer -
- ); - } - - if (state === 'Graduated') { - const stops = parseColorStops( - symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], - ); - if (stops.length === 0) { - return

No graduated symbology

; - } - - return ( - <> - {stops.map((stop, idx) => ( -
- - {stop.value.toFixed(2)} -
- ))} - - ); - } - - if (state === 'Categorized') { - const categories = parseCaseCategories( - symbology.color?.['fill-color'] || symbology.color?.['stroke-color'], - ); - if (categories.length === 0) { - return

No categorized symbology

; - } - - return ( -
- {categories.map((c, idx) => ( -
- - {String(c.category)} -
- ))} -
- ); - } - - return

Unsupported render type: {state}

; - }; - - return ( - -
- {/* Header */} -
- Legends - -
- - {/* Body */} - {!collapsed && ( -
- {isLoading &&

Loading...

} - {error &&

{error.message}

} - {!isLoading && renderLegend()} -
- )} -
-
- ); -}; - -export default Legends; diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 1a7c777cb..82caddef6 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -102,7 +102,6 @@ import { FollowIndicator } from './FollowIndicator'; import TemporalSlider from './TemporalSlider'; import { MainViewModel } from './mainviewmodel'; import { LeftPanel, RightPanel } from '../panelview'; -// import Legends from './Legends'; type OlLayerTypes = | TileLayer @@ -2300,16 +2299,6 @@ export class MainView extends React.Component { />
- {/* */} - {this._state && ( Date: Sun, 24 Aug 2025 17:54:22 +0530 Subject: [PATCH 11/18] not needed --- python/jupytergis_lab/style/base.css | 43 ---------------------------- 1 file changed, 43 deletions(-) diff --git a/python/jupytergis_lab/style/base.css b/python/jupytergis_lab/style/base.css index 136cb5fb3..1c5dbf95b 100644 --- a/python/jupytergis_lab/style/base.css +++ b/python/jupytergis_lab/style/base.css @@ -840,46 +840,3 @@ div.jGIS-toolbar-widget > div.jp-Toolbar-item:last-child { .jp-toolbar-users-item > .lm-MenuBar-itemIcon.selected { border-color: var(--jp-brand-color1); } - -.legends-container { - width: 250px; - height: 200px; - background: white; - border: 1px solid #ccc; - border-radius: 6px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); - overflow: hidden; - z-index: 1000; -} - -.legends-container.collapsed { - width: 200px; - height: 40px; -} - -.legends-header { - background: #f4f4f4; - padding: 6px; - cursor: move; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #ddd; -} - -.legends-title { - font-weight: bold; -} - -.legends-toggle { - border: none; - background: transparent; - cursor: pointer; - font-size: 14px; -} - -.legends-body { - padding: 10px; - max-height: 150px; - overflow: auto; -} From 6ea43d747b5e4062d39b37eed0a4df9bfe94731f Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Sun, 24 Aug 2025 17:55:49 +0530 Subject: [PATCH 12/18] lint --- .../base/src/panelview/components/legendItem.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx index 7918610e6..4072f6d32 100644 --- a/packages/base/src/panelview/components/legendItem.tsx +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -234,16 +234,8 @@ export const LegendItem: React.FC<{ return; } - setContent( -

- Unsupported symbology: {String(renderType)} -

, - ); + setContent(

Unsupported symbology: {String(renderType)}

); }, [symbology, isLoading, error]); - return ( -
- {content} -
- ); + return
{content}
; }; From d201a78b65ea8e05a1e48938bed0522e3b095d89 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Mon, 25 Aug 2025 16:54:45 +0530 Subject: [PATCH 13/18] simplify hook --- .../src/dialogs/symbology/hooks/useGetSymbology.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts index 3ef6c9e71..a127fcf6e 100644 --- a/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts +++ b/packages/base/src/dialogs/symbology/hooks/useGetSymbology.ts @@ -70,19 +70,7 @@ export const useGetSymbology = ({ // initial load fetchSymbology(); - model.sharedLayersChanged.connect(() => { - if (model.getLayer(layerId)) { - fetchSymbology(); - } else { - if (!disposed) { - setSymbology(null); - setIsLoading(false); - } - } - }); - model.sharedModel.awareness.on('change', () => { - console.log(`Awareness changed for layer ${layerId}`); fetchSymbology(); }); From af8d8d09b12dd232dabcf79f9b03ada942202990 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Mon, 25 Aug 2025 17:10:02 +0530 Subject: [PATCH 14/18] property label --- .../src/panelview/components/legendItem.tsx | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx index 4072f6d32..fed36b159 100644 --- a/packages/base/src/panelview/components/legendItem.tsx +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -68,6 +68,7 @@ export const LegendItem: React.FC<{ } const renderType = symbology.symbologyState?.renderType; + const property = symbology.symbologyState?.value; const fill = symbology.color?.['fill-color'] ?? symbology.color?.['circle-fill-color']; const stroke = @@ -132,24 +133,31 @@ export const LegendItem: React.FC<{ } } setContent( -
- {ranges.map((label, i) => ( -
- - {label} +
+ {property && ( +
+ {property}
- ))} + )} +
+ {ranges.map((label, i) => ( +
+ + {label} +
+ ))} +
, ); return; @@ -166,7 +174,7 @@ export const LegendItem: React.FC<{ } const n = cats.length; - const tickCount = Math.min(6, n); // limit ticks (e.g. 6 max) + const tickCount = Math.min(6, n); const tickIdx = Array.from({ length: tickCount }, (_, i) => Math.round((i * (n - 1)) / (tickCount - 1)), ); @@ -184,6 +192,11 @@ export const LegendItem: React.FC<{ setContent(
+ {property && ( +
+ {property} +
+ )}
Date: Tue, 26 Aug 2025 13:13:01 +0530 Subject: [PATCH 15/18] conditionally render legend --- .../base/src/panelview/components/layers.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/base/src/panelview/components/layers.tsx b/packages/base/src/panelview/components/layers.tsx index 0494f17c5..c3e685a0a 100644 --- a/packages/base/src/panelview/components/layers.tsx +++ b/packages/base/src/panelview/components/layers.tsx @@ -23,6 +23,7 @@ import React, { } from 'react'; import { CommandIDs, icons } from '@/src/constants'; +import { useGetSymbology } from '@/src/dialogs/symbology/hooks/useGetSymbology'; import { nonVisibilityIcon, visibilityIcon } from '@/src/icons'; import { ILeftPanelClickHandlerParams } from '@/src/panelview/leftpanel'; import { LegendItem } from './legendItem'; @@ -402,6 +403,10 @@ const LayerComponent: React.FC = props => { ); const [expanded, setExpanded] = useState(false); + const { symbology } = useGetSymbology({layerId, model: gisModel as IJupyterGISModel}); + + const hasSupportedSymbology = symbology?.symbologyState !== undefined; + const name = layer.name; useEffect(() => { @@ -460,20 +465,22 @@ const LayerComponent: React.FC = props => { onContextMenu={setSelection} style={{ display: 'flex' }} > - {/* Expand/collapse legend */} - + {/* Expand/collapse legend button (only if symbology is supported) */} + {hasSupportedSymbology && ( + + )} {/* Visibility toggle */}
- {expanded && gisModel && ( + {/* Show legend only if supported symbology */} + {expanded && gisModel && hasSupportedSymbology && (
From cb002c1c91f7fec7e7c4db6418917250fd44ecf0 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Tue, 26 Aug 2025 13:15:12 +0530 Subject: [PATCH 16/18] lint --- packages/base/src/panelview/components/layers.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/base/src/panelview/components/layers.tsx b/packages/base/src/panelview/components/layers.tsx index c3e685a0a..7909dee65 100644 --- a/packages/base/src/panelview/components/layers.tsx +++ b/packages/base/src/panelview/components/layers.tsx @@ -403,7 +403,10 @@ const LayerComponent: React.FC = props => { ); const [expanded, setExpanded] = useState(false); - const { symbology } = useGetSymbology({layerId, model: gisModel as IJupyterGISModel}); + const { symbology } = useGetSymbology({ + layerId, + model: gisModel as IJupyterGISModel, + }); const hasSupportedSymbology = symbology?.symbologyState !== undefined; From ef736957441d2961187a95b0a95b1d57bdb50287 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Wed, 27 Aug 2025 15:40:57 +0530 Subject: [PATCH 17/18] swamps for categorised and gradient for graduated --- .../src/panelview/components/legendItem.tsx | 117 ++++++++---------- 1 file changed, 49 insertions(+), 68 deletions(-) diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx index fed36b159..cf15e1951 100644 --- a/packages/base/src/panelview/components/legendItem.tsx +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -120,72 +120,11 @@ export const LegendItem: React.FC<{ setContent(

No graduated symbology

); return; } - const values = stops.map(s => s.value); - const colors = stops.map(s => s.color); - const ranges: string[] = []; - for (let i = 0; i < values.length; i++) { - if (i === 0) { - ranges.push(`< ${values[0].toFixed(2)}`); - } else if (i === values.length - 1) { - ranges.push(`> ${values[values.length - 1].toFixed(2)}`); - } else { - ranges.push(`${values[i - 1].toFixed(2)} โ€“ ${values[i].toFixed(2)}`); - } - } - setContent( -
- {property && ( -
- {property} -
- )} -
- {ranges.map((label, i) => ( -
- - {label} -
- ))} -
-
, - ); - return; - } - - // Categorized - if (renderType === 'Categorized') { - const cats = parseCaseCategories(fill || stroke); - if (!cats.length) { - setContent( -

No categorized symbology

, - ); - return; - } - - const n = cats.length; - const tickCount = Math.min(6, n); - const tickIdx = Array.from({ length: tickCount }, (_, i) => - Math.round((i * (n - 1)) / (tickCount - 1)), - ); - const uniqueIdx = Array.from(new Set(tickIdx)); - const labeled = uniqueIdx.map(i => ({ ...cats[i], idx: i })); - const segments = cats - .map((c, i) => { - const start = (i * 100) / n; - const end = ((i + 1) * 100) / n; - return `${c.color} ${start}% ${end}%`; + const segments = stops + .map((s, i) => { + const pct = (i / (stops.length - 1)) * 100; + return `${s.color} ${pct}%`; }) .join(', '); const gradient = `linear-gradient(to right, ${segments})`; @@ -208,8 +147,8 @@ export const LegendItem: React.FC<{ marginTop: 10, }} > - {labeled.map((c, i) => { - const left = (c.idx * 100) / (n - 1); + {stops.map((s, i) => { + const left = (i / (stops.length - 1)) * 100; const up = i % 2 === 0; return (
- {String(c.category)} + {s.value.toFixed(2)}
); @@ -247,6 +187,47 @@ export const LegendItem: React.FC<{ return; } + // Categorized + if (renderType === 'Categorized') { + const cats = parseCaseCategories(fill || stroke); + if (!cats.length) { + setContent( +

No categorized symbology

, + ); + return; + } + + setContent( +
+ {property && ( +
+ {property} +
+ )} +
+ {cats.map((c, i) => ( +
+ + {String(c.category)} +
+ ))} +
+
, + ); + return; + } + setContent(

Unsupported symbology: {String(renderType)}

); }, [symbology, isLoading, error]); From 2b7badbbd4dbc2fdbdd93793e52a99c299fc20f2 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Wed, 27 Aug 2025 15:46:00 +0530 Subject: [PATCH 18/18] max height and overflow for categorised --- packages/base/src/panelview/components/legendItem.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/base/src/panelview/components/legendItem.tsx b/packages/base/src/panelview/components/legendItem.tsx index cf15e1951..50fa2fbd2 100644 --- a/packages/base/src/panelview/components/legendItem.tsx +++ b/packages/base/src/panelview/components/legendItem.tsx @@ -204,7 +204,15 @@ export const LegendItem: React.FC<{ {property}
)} -
+
{cats.map((c, i) => (