diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js
index ca2a0e7ec..f6059e11a 100644
--- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js
+++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js
@@ -1,38 +1,25 @@
import React from 'react';
-import Typography from '../../src/components/atoms/Typography';
import LegendWidgetUI from '../../src/widgets/legend/LegendWidgetUI';
import { fireEvent, render, screen } from '../widgets/utils/testUtils';
-const CUSTOM_CHILDREN = Legend custom;
-
const MY_CUSTOM_LEGEND_KEY = 'my-custom-legend';
-const LAYER_OPTIONS = {
- PALETTE_SELECTOR: 'PALETTE_SELECTOR'
-};
-
-const LAYER_OPTIONS_COMPONENTS = {
- [LAYER_OPTIONS.PALETTE_SELECTOR]: PaletteSelector
-};
-
-function PaletteSelector() {
- return
PaletteSelector
;
-}
-
describe('LegendWidgetUI', () => {
const DATA = [
{
+ // 0
id: 'category',
title: 'Category Layer',
visible: true,
+ helperText: 'lorem',
legend: {
type: 'category',
- note: 'lorem',
colors: ['#000', '#00F', '#0F0'],
labels: ['Category 1', 'Category 2', 'Category 3']
}
},
{
+ // 1
id: 'icon',
title: 'Icon Layer',
visible: true,
@@ -47,6 +34,7 @@ describe('LegendWidgetUI', () => {
}
},
{
+ // 2
id: 'bins',
title: 'Ramp Layer',
visible: true,
@@ -57,6 +45,7 @@ describe('LegendWidgetUI', () => {
}
},
{
+ // 3
id: 'continuous',
title: 'Ramp Layer',
visible: true,
@@ -67,6 +56,7 @@ describe('LegendWidgetUI', () => {
}
},
{
+ // 4
id: 'proportion',
title: 'Proportion Layer',
visible: true,
@@ -76,14 +66,7 @@ describe('LegendWidgetUI', () => {
}
},
{
- id: 'custom',
- title: 'Single Layer',
- visible: true,
- legend: {
- children: CUSTOM_CHILDREN
- }
- },
- {
+ // 5
id: 'custom_key',
title: 'Single Layer',
visible: true,
@@ -95,33 +78,10 @@ describe('LegendWidgetUI', () => {
colors: ['#000', '#00F', '#0F0'],
labels: ['Category 1', 'Category 2', 'Category 3']
}
- },
- {
- id: 'custom_children',
- title: 'Single Layer',
- visible: true,
- showOpacityControl: true,
- opacity: 0.6,
- legend: {
- children: CUSTOM_CHILDREN
- }
- },
- {
- id: 'palette',
- title: 'Store types',
- visible: true,
- options: [LAYER_OPTIONS.PALETTE_SELECTOR],
- legend: {
- children: CUSTOM_CHILDREN
- }
}
];
- const Widget = (props) => ;
- test('single legend', () => {
- render();
- expect(screen.queryByText('Layers')).not.toBeInTheDocument();
- });
+ const Widget = (props) => ;
test('multiple legends', () => {
render();
@@ -131,10 +91,33 @@ describe('LegendWidgetUI', () => {
test('multiple legends with collapsed as true', () => {
render();
- expect(screen.queryByText('Layers')).toBeInTheDocument();
+ // expanded legend toggle
+ expect(screen.queryByText('Layers')).not.toBeInTheDocument();
+ // collapsed legend toggle
+ expect(screen.queryByLabelText('Layers')).toBeInTheDocument();
expect(screen.queryByTestId('categories-legend')).not.toBeInTheDocument();
});
+ test('layer with no legend is not shown on widget', () => {
+ const layers = [
+ {
+ id: 'test-layer-no-legend',
+ title: 'Test layer hidden',
+ visible: true
+ },
+ {
+ id: 'test-layer-no-legend-2',
+ title: 'Test layer shown',
+ visible: true,
+ legend: {}
+ }
+ ];
+ render();
+ expect(screen.queryByText('Layers')).toBeInTheDocument();
+ expect(screen.queryByText('Test layer hidden')).not.toBeInTheDocument();
+ expect(screen.queryByText('Test layer shown')).toBeInTheDocument();
+ });
+
test('Category legend', () => {
render();
expect(screen.getByTestId('categories-legend')).toBeInTheDocument();
@@ -160,11 +143,6 @@ describe('LegendWidgetUI', () => {
expect(screen.getByTestId('proportion-legend')).toBeInTheDocument();
});
- test('Custom legend', () => {
- render();
- expect(screen.getByText('Legend custom')).toBeInTheDocument();
- });
-
test('Empty legend', () => {
const EMPTY_LAYER = { id: 'empty', title: 'Empty Layer', legend: {} };
render();
@@ -184,42 +162,53 @@ describe('LegendWidgetUI', () => {
test('with custom legend types', () => {
const MyCustomLegendComponent = jest.fn();
MyCustomLegendComponent.mockReturnValue(Test
);
+
render(
);
+
expect(MyCustomLegendComponent).toHaveBeenCalled();
expect(MyCustomLegendComponent).toHaveBeenCalledWith(
- { layer: DATA[6], legend: DATA[6].legend },
+ { layer: DATA[5], legend: DATA[5].legend },
{}
);
expect(screen.getByText('Test')).toBeInTheDocument();
});
test('legend with opacity control', async () => {
- const legendConfig = DATA[7];
+ const legendConfig = {
+ id: 'test-opacity-control',
+ title: 'Test opacity control',
+ visible: true,
+ showOpacityControl: true,
+ opacity: 0.8,
+ legend: {}
+ };
const onChangeOpacity = jest.fn();
const container = render(
);
- const layerOptionsBtn = await screen.findByLabelText('Layer options');
- expect(layerOptionsBtn).toBeInTheDocument();
- layerOptionsBtn.click();
- expect(screen.getByText('Opacity')).toBeInTheDocument();
+
+ const toggleButton = screen.getByRole('button', { name: 'Opacity' });
+ expect(toggleButton).toBeInTheDocument();
+ toggleButton.click();
const opacitySelectorInput = container.getByTestId('opacity-slider');
- expect(opacitySelectorInput.value).toBe('' + legendConfig.opacity * 100);
+ expect(opacitySelectorInput).toBeInTheDocument();
+
+ expect(opacitySelectorInput.value).toBe(String(legendConfig.opacity * 100));
- fireEvent.change(opacitySelectorInput, { target: { value: '50' } });
+ fireEvent.change(opacitySelectorInput, { target: { value: 50 } });
expect(onChangeOpacity).toHaveBeenCalledTimes(1);
expect(onChangeOpacity).toHaveBeenCalledWith({ id: legendConfig.id, opacity: 0.5 });
});
test('should manage legend collapsed state correctly', () => {
- let legendConfig = { ...DATA[7], legend: { ...DATA[7].legend, collapsed: true } };
+ let legendConfig = { ...DATA[0], collapsed: true };
const onChangeLegendRowCollapsed = jest.fn();
const { rerender } = render(
@@ -229,11 +218,11 @@ describe('LegendWidgetUI', () => {
>
);
- expect(screen.queryByText('Legend custom')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('legend-layer-variable-list')).not.toBeInTheDocument();
- const layerOptionsBtn = screen.getByText('Single Layer');
- expect(layerOptionsBtn).toBeInTheDocument();
- layerOptionsBtn.click();
+ const toggleButton = screen.getByRole('button', { name: 'Expand layer' });
+ expect(toggleButton).toBeInTheDocument();
+ toggleButton.click();
expect(onChangeLegendRowCollapsed).toHaveBeenCalledTimes(1);
expect(onChangeLegendRowCollapsed).toHaveBeenCalledWith({
@@ -241,7 +230,8 @@ describe('LegendWidgetUI', () => {
collapsed: false
});
- legendConfig = { ...DATA[7], legend: { ...DATA[7].legend, collapsed: false } };
+ legendConfig = { ...DATA[0], collapsed: false };
+
rerender(
{
>
);
- expect(screen.getByText('Legend custom')).toBeInTheDocument();
+ expect(screen.getByTestId('legend-layer-variable-list')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Collapse layer' })).toBeInTheDocument();
});
- test('with custom layer options', async () => {
- const layer = DATA[8];
- render(
-
- );
- const layerOptionsBtn = await screen.findByLabelText('Layer options');
- expect(layerOptionsBtn).toBeInTheDocument();
- layerOptionsBtn.click();
- expect(screen.getByText('PaletteSelector')).toBeInTheDocument();
- });
-
- test('with custom layer options - unknown option', async () => {
- const layer = { ...DATA[8], options: ['unknown'] };
- render(
-
- );
- const layerOptionsBtn = await screen.findByLabelText('Layer options');
- expect(layerOptionsBtn).toBeInTheDocument();
- layerOptionsBtn.click();
- expect(screen.getByText('Unknown layer option')).toBeInTheDocument();
+ test('helper text', () => {
+ render();
+ expect(screen.getByText('helperText')).toBeInTheDocument();
});
});
diff --git a/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js b/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js
index 914a035b9..250570fee 100644
--- a/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js
+++ b/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '../utils/testUtils';
-import LegendCategories from '../../../src/widgets/legend/LegendCategories';
+import LegendCategories from '../../../src/widgets/legend/legend-types/LegendCategories';
import { getPalette } from '../../../src/utils/palette';
import { hexToRgb } from '@mui/material';
diff --git a/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js b/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js
index 69605b83c..9ad8b5187 100644
--- a/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js
+++ b/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '../../widgets/utils/testUtils';
-import LegendIcon from '../../../src/widgets/legend/LegendIcon';
+import LegendIcon from '../../../src/widgets/legend/legend-types/LegendIcon';
const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`;
const ICON_2 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gNzAK/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8AAEQgCAAIAAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A9gooopDCiiigCWiiikAUUUUAR0UUUigooooAfRRRQIKKKKAFooopiCiiigBaKKKBBRRRQA2iiirJCiiigCOiiimZhRRRQAtFFFcRqFFFFAC0UUV2lhRRRQA6iiikQFFFFAEdFFFBgFFFFAD6KKKyNgooooAKKKK2NAooooAWiiikIKKKKAG0UUUxhRRRQA2iiioEFFFFAEtFFFIAooooAjooopFBRRRQA+iiigQUUUUALRRRTEFFFFAC0UUUCCiiigBtFFFWSFFFFAEdFFFMzCiiigBaKKK4jUKKKKAFooortLCiiigB1FFFIgKKKKAI6KKKDAKKKKAH0UUVkbBRRRQAUUUVsaBRRRQAtFFFIQUUUUANooopjCiiigCSiiisyQooooAbRRRVkhRRRQAtFFFABRRRQBBRRRVgFFFFAE9FFFQWFFFFAFSiiisTUKKKKAEooorAsKKKKAGUUUViUFFFFAFeiiitjYKKKKAJaKKK6jkCiiigDRoooqzMKKKKAKtFFFQaBRRRQBcooopmIUUUUAV6KKKg0CiiigCeiiiqMwooooAiooorU5QooooAmooorI6gooooAbRRRVkhRRRQAtFFFABRRRQBBRRRVgFFFFAE9FFFQWFFFFAFSiiisTUKKKKAEooorAsKKKKAGUUUViUFFFFAFeiiitjYKKKKAJaKKK6jkCiiigDRoooqzMKKKKAKtFFFQaBRRRQBcooopmIUUUUAV6KKKg0CiiigCeiiiqMwooooAiooorU5QooooAdRRRSNAooooAWiiigYUUUUANooorA3CiiigBKKKK5igooooA4uiiivNPRCiiigDNooorzzqCiiigChRRRXCdAUUUUAOoooqCwooooAWiiigzCiiigBaKKKRyhRRRQBPRRRXWAUUUUAdBRRRXqkhRRRQB01FFFeweWFFFFAE1FFFdhmFFFFAEdFFFUaBRRRQBJRRRUkBRRRQAUUUUHMFFFFAC0UUUDCiiigBtFFFYG4UUUUAJRRRXMUFFFFAHF0UUV5p6IUUUUAZtFFFeedQUUUUAUKKKK4ToCiiigB1FFFQWFFFFAC0UUUGYUUUUALRRRSOUKKKKAJ6KKK6wCiiigDoKKKK9UkKKKKAOmooor2DywooooAmooorsMwooooAjoooqjQKKKKAJKKKKkgKKKKACiiigQUUUUAFFFFcwBRRRQBVooorzDoCiiigDmqKKK809AKKKKAIaKKKkoKKKKALlFFFdhAUUUUAaNFFFegYhRRRQBo0UUV6BxhRRRQBHRRRWBkFFFFAEdFFFcB1hRRRQBz1FFFcp1hRRRQBlUUUVwnQFFFFAGfRRRWYwooooA72iiivoTyQooooA2KKKK9U80KKKKAI6KKK4SwooooAkoooruNgooooAKKKK5gCiiigCrRRRXmHQFFFFAHNUUUV5p6AUUUUAQ0UUVJQUUUUAXKKKK7CAooooA0aKKK9AxCiiigDRooor0DjCiiigCOiiisDIKKKKAI6KKK4DrCiiigDnqKKK5TrCiiigDKooorhOgKKKKAM+iiisxhRRRQB3tFFFfQnkhRRRQBsUUUV6p5oUUUUAR0UUVwlhRRRQBPRRRXYWFFFFACUUUVJYUUUUAcbRRRQbhRRRQBxVFFFIoKKKKAO4ooopiCiiigDrqKKKDnCiiigBKKKKsxCiiigBlFFFMxCiiigCSiiig7AooooAbRRRQMKKKKAFooooEFFFFAGPRRRXgncFFFFAHFUUUV4p7QUUUUAZ1FFFamYUUUUAeg0UUV7Z44UUUUAdBRRRXqHEFFFFABRRRVEhRRRQAlFFFSWFFFFAHG0UUUG4UUUUAcVRRRSKCiiigDuKKKKYgooooA66iiig5wooooASiiirMQooooAZRRRTMQooooAkooooOwKKKKAG0UUUDCiiigBaKKKBBRRRQBj0UUV4J3BRRRQBxVFFFeKe0FFFFAGdRRRWpmFFFFAHoNFFFe2eOFFFFAHQUUUV6hxBRRRQA2iiitDIKKKKAPHaKKKzJCiiigDyKiiipAKKKKAIaKKKosKKKKAGUUUUxhRRRQB3tFFFUMKKKKAPoqiiimWFFFFABRRRQAUUUUAPooopGYUUUUAR0UUVJqFFFFAHEUUUV4R6YUUUUAZ9FFFeeegFFFFAEVFFFZm4UUUUAZ1FFFcpqFFFFAF6iiitTmCiiigD0yiiivuz5sKKKKAJaKKKk5gooooA8dooorMkKKKKAPIqKKKkAooooAhoooqiwooooAZRRRTGFFFFAHe0UUVQwooooA+iqKKKZYUUUUAFFFFABRRRQA+iiikZhRRRQBHRRRUmoUUUUAcRRRRXhHphRRRQBn0UUV556AUUUUARUUUVmbhRRRQBnUUUVymoUUUUAXqKKK1OYKKKKAPTKKKK+7PmwooooA46iiisTUKKKKAPnCiiiqICiiigD2CiiiqMwooooA9hoooqiAooooA5CiiimSFFFFAHzTRRRUjCiiigD1CiiigoKKKKAPeaKKKQwooooAKKKK5yAooooA5OiiivAPTCiiigDkqKKK4jsCiiigDvKKKK9484KKKKAN2iiivSPPCiiigDJooorwD0gooooA8xooorzz0wooooA9Cooor2znCiiigDLooor1DzwooooA+cKKKKogKKKKAPYKKKKozCiiigD2GiiiqICiiigDkKKKKZIUUUUAfNNFFFSMKKKKAPUKKKKCgooooA95ooopDCiiigAooornICiiigDk6KKK8A9MKKKKAOSoooriOwKKKKAO8ooor3jzgooooA3aKKK9I88KKKKAMmiiivAPSCiiigDzGiiivPPTCiiigD0KiiivbOcKKKKAPCqKKK9U8AKKKKAMeiiimUFFFFAH1lRRRQUFFFFADaKKKZYUUUUALRRRSEFFFFAHjFFFFQAUUUUAeO0UUUyAooooA+x6KKKzGFFFFACUUUUywooooA8tooor5A+nCiiigDpKKKK7DAKKKKAOwooor6k+aCiiigBlFFFMyCiiigBtFFFcB2hRRRQBxFFFFeaegFFFFACUUUV5hYUUUUAeSUUUV9ieEFFFFAGPRRRTKCiiigD6yooooKCiiigBtFFFMsKKKKAFooopCCiiigDxiiiioAKKKKAPHaKKKZAUUUUAfY9FFFZjCiiigBKKKKZYUUUUAeW0UUV8gfThRRRQB0lFFFdhgFFFFAHYUUUV9SfNBRRRQAyiiimZBRRRQA2iiiuA7QooooA4iiiivNPQCiiigBKKKK8wsKKKKAPKaKKK+yPHCiiigDAooooAKKKKAPrOiiigAooooAWiiigAooooAKKKKACiiigDwWiiigAooooA8xooooAKKKKAPsaiiigAooooASiiisygooooA8rooor4g9wKKKKAOlooorrJCiiigDsKKKK+xPCCiiigBtFFFMyCiiigCCiiiuM7gooooA4aiiivKPaCiiigCxRRRXCc4UUUUAeTUUUV9aeKFFFFAGBRRRQAUUUUAfWdFFFABRRRQAtFFFABRRRQAUUUUAFFFFAHgtFFFABRRRQB5jRRRQAUUUUAfY1FFFABRRRQAlFFFZlBRRRQB5XRRRXxB7gUUUUAdLRRRXWSFFFFAHYUUUV9ieEFFFFADaKKKZkFFFFAEFFFFcZ3BRRRQBw1FFFeUe0FFFFAFiiiiuE5wooooAo0UUV9aeKFFFFAHgNFFFABRRRQB6tRRRQAUUUUAen0UUUAFFFFAHA0UUUAFFFFAHjFFFFABRRRQB67RRRQAUUUUAe10UUUAFFFFADaKKKZAUUUUAcxRRRXinshRRRQBxFFFFeWdoUUUUAd1RRRXonEFFFFAGvRRRXpHGFFFFAGLRRRXjHqBRRRQBwlFFFchYUUUUAel0UUV7RwBRRRQBx1FFFeoeaFFFFAHgNFFFABRRRQB6tRRRQAUUUUAen0UUUAFFFFAHA0UUUAFFFFAHjFFFFABRRRQB67RRRQAUUUUAe10UUUAFFFFADaKKKZAUUUUAcxRRRXinshRRRQBxFFFFeWdoUUUUAd1RRRXonEFFFFAGvRRRXpHGFFFFAGLRRRXjHqBRRRQBwlFFFchYUUUUAel0UUV7RwBRRRQBq0UUV6h5oUUUUAfJFFFFABRRRQBnUUUUAFFFFAGhRRRQAUUUUAR0UUUAFFFFAHplFFFABRRRQB7pRRRQAUUUUANooooAKKKKAIaKKKyOUKKKKAH0UUVkdoUUUUAc1RRRXzx7QUUUUAc7RRRXEMKKKKALdFFFdBIUUUUAZVFFFcIwooooAfRRRTICiiigDraKKK+oOcKKKKAOiooor1TzQooooA+SKKKKACiiigDOooooAKKKKANCiiigAooooAjooooAKKKKAPTKKKKACiiigD3SiiigAooooAbRRRQAUUUUAQ0UUVkcoUUUUAPooorI7QooooA5qiiivnj2gooooA52iiiuIYUUUUAW6KKK6CQooooAyqKKK4RhRRRQA+iiimQFFFFAHW0UUV9Qc4UUUUAdJRRRXqnmhRRRQBztFFFABRRRQB8r0UUUAFFFFABRRRQAUUUUAfUlFFFABRRRQB1FFFFABRRRQBXooooAKKKKAJqKKKACiiigBtFFFUSFFFFABRRRQaBRRRQBFRRRVmgUUUUAZVFFFcZ0BRRRQBxlFFFcZAUUUUAaVFFFIkKKKKANuiiiuoYUUUUAblFFFdZkFFFFAE1FFFIyCiiigDnaKKKACiiigD5XooooAKKKKACiiigAooooA+pKKKKACiiigDqKKKKACiiigCvRRRQAUUUUATUUUUAFFFFADaKKKokKKKKACiiig0CiiigCKiiirNAooooAyqKKK4zoCiiigDjKKKK4yAooooA0qKKKRIUUUUAbdFFFdQwooooA3KKKK6zIKKKKAJKKKKRkFFFFAElFFFABRRRQBytFFFABRRRQB4VRRRQAUUUUAclRRRQAUUUUAdrRRRQAUUUUAd5RRRQAUUUUAdHRRRQAUUUUAV6KKKosKKKKAMKiiisjMKKKKAOJooorrAKKKKAOIooopAFFFFAGtRRRXGIKKKKAPf6KKK6TMKKKKAOzoooqDUKKKKAEooorlLCiiigBKKKK6SQooooAkooooAKKKKAOVooooAKKKKAPCqKKKACiiigDkqKKKACiiigDtaKKKACiiigDvKKKKACiiigDo6KKKACiiigCvRRRVFhRRRQBhUUUVkZhRRRQBxNFFFdYBRRRQBxFFFFIAooooA1qKKK4xBRRRQB7/RRRXSZhRRRQB2dFFFQahRRRQAlFFFcpYUUUUAPooorpJCiiigCtRRRQAUUUUAWaKKKACiiigCvRRRQAUUUUAeV0UUUAFFFFAHl9FFFABRRRQBz9FFFABRRRQAUUUUAFFFFAElFFFQUFFFFAEdFFFUIKKKKAOgooopDCiiigD0miiigsKKKKAPYaKKKszCiiigBKKKKZAUUUUAJRRRQIKKKKAG0UUVzjCiiigCaiiitigooooArUUUUAFFFFAFmiiigAooooAr0UUUAFFFFAHldFFFABRRRQB5fRRRQAUUUUAc/RRRQAUUUUAFFFFABRRRQBJRRRUFBRRRQBHRRRVCCiiigDoKKKKQwooooA9JooooLCiiigD2GiiirMwooooASiiimQFFFFACUUUUCCiiigBtFFFc4wooooAlooorYoKKKKAFooooAKKKKAI6KKKACiiigCSiiigAooooAKKKKACiiigCGiiigAooooAbRRRQAUUUUAZdFFFABRRRQA+iiishBRRRQBq0UUVuWFFFFAEVFFFYlBRRRQBNRRRWJIUUUUAFFFFdYBRRRQAlFFFMAooooAWiiiggKKKKACiiipJCiiigB1FFFBYUUUUALRRRQAUUUUAR0UUUAFFFFAElFFFABRRRQAUUUUAFFFFAENFFFABRRRQA2iiigAooooAy6KKKACiiigB9FFFZCCiiigDVooorcsKKKKAIqKKKxKCiiigCaiiisSQooooAKKKK6wCiiigBKKKKYBRRRQAtFFFBAUUUUAFFFFSSFFFFABRRRQWFFFFAD6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBKKKKACiiigBaKKKACiiigBtFFFABRRRQAUUUUyAooooAbRRRTNAooooAfRRRUiCiiigCOiiipMgooooAfRRRTLCiiigBKKKKZoFFFFAC0UUUwCiiigBtFFFQZhRRRQAtFFFUUFFFFAD6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBKKKKACiiigBaKKKACiiigBtFFFABRRRQAUUUUyAooooAbRRRTNAooooAfRRRUiCiiigCOiiipMgooooAfRRRTLCiiigBKKKKZoFFFFAC0UUUwCiiigBtFFFQZhRRRQB//Z`;
diff --git a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js
index 3a2758ed9..72f81d297 100644
--- a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js
+++ b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '../../widgets/utils/testUtils';
-import LegendProportion from '../../../src/widgets/legend/LegendProportion';
+import LegendProportion from '../../../src/widgets/legend/legend-types/LegendProportion';
+import { IntlProvider } from 'react-intl';
const DEFAULT_LEGEND = {
labels: ['0', '200']
@@ -8,12 +9,29 @@ const DEFAULT_LEGEND = {
describe('LegendProportion', () => {
test('renders correctly', () => {
- render();
+ render(
+
+
+
+ );
expect(screen.queryByText('Max: 200')).toBeInTheDocument();
expect(screen.queryByText('150')).toBeInTheDocument();
expect(screen.queryByText('50')).toBeInTheDocument();
expect(screen.queryByText('Min: 0')).toBeInTheDocument();
});
+ test('renders correctly without min and max', () => {
+ render(
+
+
+
+ );
+ expect(screen.queryByText('Max')).not.toBeInTheDocument();
+ expect(screen.queryByText('Min')).not.toBeInTheDocument();
+ expect(screen.queryByText('200')).toBeInTheDocument();
+ expect(screen.queryByText('150')).toBeInTheDocument();
+ expect(screen.queryByText('50')).toBeInTheDocument();
+ expect(screen.queryByText('0')).toBeInTheDocument();
+ });
test('renders error if neither labels is defined', () => {
render();
expect(
diff --git a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js
index 8edfd8d8d..89831bb15 100644
--- a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js
+++ b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render, screen } from '../../widgets/utils/testUtils';
-import LegendRamp from '../../../src/widgets/legend/LegendRamp';
+import LegendRamp from '../../../src/widgets/legend/legend-types/LegendRamp';
import { getPalette } from '../../../src/utils/palette';
import { hexToRgb } from '@mui/material';
@@ -41,6 +41,13 @@ describe('LegendRamp', () => {
expect(backgroundColor).toBe(hexToRgb(color));
});
});
+ test('renders correctly without min and max', () => {
+ render();
+ expect(screen.queryByText('< 0')).not.toBeInTheDocument();
+ expect(screen.queryByText('≥ 200')).not.toBeInTheDocument();
+ expect(screen.queryByText('0')).toBeInTheDocument();
+ expect(screen.queryByText('200')).toBeInTheDocument();
+ });
test('renders formatted labels correctly', () => {
render();
expect(screen.queryByText('< 0 km')).toBeInTheDocument();
diff --git a/packages/react-ui/src/assets/icons/OpacityIcon.js b/packages/react-ui/src/assets/icons/OpacityIcon.js
new file mode 100644
index 000000000..437e7691a
--- /dev/null
+++ b/packages/react-ui/src/assets/icons/OpacityIcon.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { SvgIcon } from '@mui/material';
+
+export default function OpacityIcon(props) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/react-ui/src/index.d.ts b/packages/react-ui/src/index.d.ts
index 2418b1b68..ed9d8cc64 100644
--- a/packages/react-ui/src/index.d.ts
+++ b/packages/react-ui/src/index.d.ts
@@ -9,11 +9,12 @@ import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI';
import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI';
import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI';
import PieWidgetUI from './widgets/PieWidgetUI/PieWidgetUI';
-import LegendWidgetUI, { LEGEND_TYPES } from './widgets/legend/LegendWidgetUI';
-import LegendCategories from './widgets/legend/LegendCategories';
-import LegendIcon from './widgets/legend/LegendIcon';
-import LegendProportion from './widgets/legend/LegendProportion';
-import LegendRamp from './widgets/legend/LegendRamp';
+import LegendWidgetUI from './widgets/legend/LegendWidgetUI';
+import LEGEND_TYPES from './widgets/legend/legend-types/LegendTypes'
+import LegendCategories from './widgets/legend/legend-types/LegendCategories';
+import LegendIcon from './widgets/legend/legend-types/LegendIcon';
+import LegendProportion from './widgets/legend/legend-types/LegendProportion';
+import LegendRamp from './widgets/legend/legend-types/LegendRamp';
import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI';
import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI';
import {
@@ -136,4 +137,4 @@ export {
Alert,
AlertProps,
CartoAlertSeverity
-};
+};
\ No newline at end of file
diff --git a/packages/react-ui/src/index.js b/packages/react-ui/src/index.js
index 9cccfa6a9..aaf65a637 100644
--- a/packages/react-ui/src/index.js
+++ b/packages/react-ui/src/index.js
@@ -5,11 +5,12 @@ import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI';
import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI';
import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI';
import PieWidgetUI from './widgets/PieWidgetUI/PieWidgetUI';
-import LegendWidgetUI, { LEGEND_TYPES } from './widgets/legend/LegendWidgetUI';
-import LegendCategories from './widgets/legend/LegendCategories';
-import LegendIcon from './widgets/legend/LegendIcon';
-import LegendProportion from './widgets/legend/LegendProportion';
-import LegendRamp from './widgets/legend/LegendRamp';
+import LegendWidgetUI from './widgets/legend/LegendWidgetUI';
+import LEGEND_TYPES from './widgets/legend/legend-types/LegendTypes';
+import LegendCategories from './widgets/legend/legend-types/LegendCategories';
+import LegendIcon from './widgets/legend/legend-types/LegendIcon';
+import LegendProportion from './widgets/legend/legend-types/LegendProportion';
+import LegendRamp from './widgets/legend/legend-types/LegendRamp';
import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI';
import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI';
import {
diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js
index 5d74b099f..c9082407d 100644
--- a/packages/react-ui/src/localization/en.js
+++ b/packages/react-ui/src/localization/en.js
@@ -32,14 +32,33 @@ const locales = {
clear: 'Clear'
},
legend: {
- by: 'By {attr}',
layerOptions: 'Layer options',
hide: 'Hide',
show: 'Show',
layer: 'layer',
opacity: 'Opacity',
hideLayer: 'Hide layer',
- showLayer: 'Show layer'
+ showLayer: 'Show layer',
+ open: 'Open legend',
+ close: 'Close',
+ collapse: 'Collapse layer',
+ expand: 'Expand layer',
+ zoomLevel: 'Zoom level',
+ zoomLevelTooltip: 'This layer is only visible at certain zoom levels',
+ lowerThan: 'lower than',
+ greaterThan: 'greater than',
+ and: 'and',
+ zoomNote: 'Note: this layer will display at zoom levels',
+ notSupported: 'is not a known legend type',
+ subtitles: {
+ proportion: 'Radius range by',
+ icon: 'Icon based on',
+ strokeColor: 'Stroke color based on',
+ color: 'Color based on'
+ },
+ max: 'Max',
+ min: 'Min',
+ maxCategories: 'Legend limited to {n} categories'
},
range: {
clear: 'Clear',
diff --git a/packages/react-ui/src/localization/es.js b/packages/react-ui/src/localization/es.js
index 7168d0df7..01396e5a7 100644
--- a/packages/react-ui/src/localization/es.js
+++ b/packages/react-ui/src/localization/es.js
@@ -32,14 +32,32 @@ const locales = {
clear: 'Limpiar'
},
legend: {
- by: 'Por {attr}',
layerOptions: 'Opciones de capa',
hide: 'Ocultar',
show: 'Mostrar',
layer: 'capa',
opacity: 'Opacidad',
hideLayer: 'Ocultar capa',
- showLayer: 'Mostrar capa'
+ showLayer: 'Mostrar capa',
+ open: 'Abrir leyenda',
+ close: 'Cerrar',
+ collapse: 'Colapsar capa',
+ expand: 'Expandir capa',
+ zoomLevel: 'Nivel de zoom',
+ zoomLevelTooltip: 'Esta capa solo es visible a ciertos niveles de zoom',
+ lowerThan: 'menor que',
+ greaterThan: 'mayor que',
+ and: 'y',
+ zoomNote: 'Nota: esta capa se mostrará a un nivel de zoom',
+ notSupported: 'no es un tipo de leyenda conocido',
+ subtitles: {
+ proportion: 'Tamaño basado en',
+ icon: 'Icono basado en',
+ strokeColor: 'Color del borde basado en',
+ color: 'Color basado en'
+ },
+ max: 'Max',
+ min: 'Min'
},
range: {
clear: 'Limpiar',
diff --git a/packages/react-ui/src/localization/id.js b/packages/react-ui/src/localization/id.js
index 5235aeb14..1ba1895e2 100644
--- a/packages/react-ui/src/localization/id.js
+++ b/packages/react-ui/src/localization/id.js
@@ -33,14 +33,33 @@ const locales = {
clear: 'Bersihkan'
},
legend: {
- by: 'Dengan {attr}',
- layerOptions: 'Opsi lapisan',
+ layerOptions: 'Opsi Layer',
hide: 'Sembunyikan',
show: 'Tampilkan',
- layer: 'lapisan',
+ layer: 'layer',
opacity: 'Opasitas',
- hideLayer: 'Sembunyikan lapisan',
- showLayer: 'Tampilkan lapisan'
+ hideLayer: 'Sembunyikan layer',
+ showLayer: 'Tampilkan layer',
+ open: 'Buka legenda',
+ close: 'Tutup',
+ collapse: 'Ciutkan layer',
+ expand: 'Perluas layer',
+ zoomLevel: 'Tingkat Zoom',
+ zoomLevelTooltip: 'Layer ini hanya terlihat pada tingkat zoom tertentu',
+ lowerThan: 'lebih rendah dari',
+ greaterThan: 'lebih tinggi dari',
+ and: 'dan',
+ zoomNote: 'Catatan: layer ini akan ditampilkan pada tingkat zoom',
+ notSupported: 'bukan jenis legenda yang dikenal',
+ subtitles: {
+ proportion: 'Rentang radius berdasarkan',
+ icon: 'Ikon berdasarkan',
+ strokeColor: 'Warna garis berdasarkan',
+ color: 'Warna berdasarkan'
+ },
+ max: 'Maks',
+ min: 'Min',
+ maxCategories: 'Legenda terbatas pada {n} kategori'
},
range: {
clear: 'Bersihkan',
diff --git a/packages/react-ui/src/types.d.ts b/packages/react-ui/src/types.d.ts
index 31c04715f..f21729de7 100644
--- a/packages/react-ui/src/types.d.ts
+++ b/packages/react-ui/src/types.d.ts
@@ -7,6 +7,21 @@ export { AccordionGroupProps } from './components/molecules/AccordionGroup';
export { UploadFieldProps } from './components/molecules/UploadField/UploadField';
export { UploadFieldBaseProps } from './components/molecules/UploadField/UploadFieldBase';
export { AppBarProps } from './components/organisms/AppBar/AppBar';
+export {
+ LegendBins,
+ LegendCategories,
+ LegendIcons,
+ LegendRamp,
+ LegendProportion,
+ LegendWidgetUIProps,
+ LegendSelectConfig,
+ CustomLegendComponent,
+ LegendLayerData,
+ LegendLayerVariableData,
+ LegendLayerVariableBase,
+ LegendColors,
+ LegendNumericLabels
+} from './widgets/legend/LegendWidgetUI'
export type WrapperWidgetUI = {
title: string;
@@ -204,36 +219,6 @@ export type FeatureSelectionUIToggleButton = {
tooltipPlacement?: 'bottom' | 'left' | 'right' | 'top';
};
-// Legends
-export type LegendCategories = {
- legend: {
- labels?: (string | number)[];
- colors?: string | string[] | number[][];
- isStrokeColor?: boolean;
- };
-};
-
-export type LegendIcon = {
- legend: {
- labels?: string[];
- icons?: string[];
- };
-};
-
-export type LegendProportion = {
- legend: {
- labels?: (number | string)[];
- };
-};
-
-export type LegendRamp = {
- isContinuous?: boolean;
- legend: {
- labels?: (number | string)[];
- colors?: string | string[] | number[][];
- };
-};
-
export type AnimationOptions = {
duration?: number;
animateOnMount?: boolean;
diff --git a/packages/react-ui/src/widgets/legend/LayerOptionWrapper.js b/packages/react-ui/src/widgets/legend/LayerOptionWrapper.js
deleted file mode 100644
index d1378ae6d..000000000
--- a/packages/react-ui/src/widgets/legend/LayerOptionWrapper.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import { Box } from '@mui/material';
-import Typography from '../../components/atoms/Typography';
-
-export default function LayerOptionWrapper({ label, children }) {
- return (
-
-
- {label}
-
- {children}
-
- );
-}
diff --git a/packages/react-ui/src/widgets/legend/LegendCategories.js b/packages/react-ui/src/widgets/legend/LegendCategories.js
deleted file mode 100644
index 10ef61be4..000000000
--- a/packages/react-ui/src/widgets/legend/LegendCategories.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react';
-import { Box, Grid, Tooltip, styled } from '@mui/material';
-import { getPalette } from '../../utils/palette';
-import PropTypes from 'prop-types';
-import Typography from '../../components/atoms/Typography';
-
-function LegendCategories({ legend }) {
- const {
- labels = [],
- colors = [],
- isStrokeColor = false,
- customMarkers,
- maskedMarkers = true
- } = legend;
-
- const palette = getPalette(colors, labels.length);
-
- const Rows = labels.map((label, idx) => (
-
- ));
-
- return (
-
- {Rows}
-
- );
-}
-
-LegendCategories.defaultProps = {
- legend: {
- labels: [],
- colors: [],
- isStrokeColor: false
- }
-};
-
-const ColorType = PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.arrayOf(PropTypes.number)
-]);
-
-LegendCategories.propTypes = {
- legend: PropTypes.shape({
- labels: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
- colors: PropTypes.oneOfType([PropTypes.arrayOf(ColorType), PropTypes.string]),
- customMarkers: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.string),
- PropTypes.string
- ]),
- maskedMarkers: PropTypes.bool,
- isStrokeColor: PropTypes.bool
- }).isRequired
-};
-
-export default LegendCategories;
-
-const getCircleStyles = ({ isMax, color, isStrokeColor, theme }) => ({
- border: '2px solid transparent',
- '&::after': {
- position: 'absolute',
- display: isMax ? 'block' : 'none',
- content: '""',
- width: theme.spacing(2),
- height: theme.spacing(2),
- border: `2px solid ${theme.palette.grey[900]}`,
- transform: 'translate(-30%, -30%)',
- borderRadius: '50%',
- boxSizing: 'content-box'
- },
- ...(isStrokeColor ? { borderColor: color } : { backgroundColor: color })
-});
-
-const getIconStyles = ({ icon, color, maskedIcon }) => ({
- maskRepeat: 'no-repeat',
- maskSize: 'cover',
- backgroundRepeat: 'no-repeat',
- backgroundSize: 'cover',
- ...(maskedIcon
- ? {
- backgroundColor: color,
- maskImage: `url(${icon})`,
- WebkitMaskImage: `url(${icon})`
- }
- : {
- backgroundColor: `rgba(0,0,0,0)`,
- backgroundImage: `url(${icon})`
- })
-});
-
-const Marker = styled(Box, {
- shouldForwardProp: (prop) =>
- !['isMax', 'icon', 'maskedIcon', 'color', 'isStrokeColor'].includes(prop)
-})(({ isMax, icon, maskedIcon, color, isStrokeColor, theme }) => ({
- whiteSpace: 'nowrap',
- display: 'block',
- width: theme.spacing(1.5),
- height: theme.spacing(1.5),
- borderRadius: '50%',
- position: 'relative',
- border: '2px solid transparent',
- ...(icon
- ? getIconStyles({ icon, color, maskedIcon })
- : getCircleStyles({ isMax, color, isStrokeColor, theme }))
-}));
-
-const LongTruncate = styled(Typography)(() => ({
- flex: 1,
- whiteSpace: 'nowrap',
- overflow: 'hidden',
- textOverflow: 'ellipsis'
-}));
-
-const TitlePhantom = styled(LongTruncate)(() => ({
- opacity: 0,
- position: 'absolute',
- whiteSpace: 'nowrap',
- pointerEvents: 'none'
-}));
-
-function Row({ label, isMax, isStrokeColor, color = '#000', icon, maskedIcon }) {
- const [showTooltip, setShowTooltip] = useState(false);
- const labelRef = useRef(null);
- const labelPhantomRef = useRef(null);
-
- useEffect(() => {
- if (!labelPhantomRef?.current || !labelRef?.current) {
- return;
- }
- const labelSizes = labelRef?.current.getBoundingClientRect();
- const labelPhantomSizes = labelPhantomRef?.current.getBoundingClientRect();
- setShowTooltip(labelPhantomSizes.width > labelSizes.width);
- }, [setShowTooltip, labelPhantomRef, labelRef]);
-
- return (
-
-
-
-
-
-
- {label}
-
-
- {label}
-
-
-
- );
-}
diff --git a/packages/react-ui/src/widgets/legend/LegendIcon.js b/packages/react-ui/src/widgets/legend/LegendIcon.js
deleted file mode 100644
index 7e67aa095..000000000
--- a/packages/react-ui/src/widgets/legend/LegendIcon.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import { Box, Grid } from '@mui/material';
-import PropTypes from 'prop-types';
-import Typography from '../../components/atoms/Typography';
-import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants';
-
-function LegendIcon({ legend }) {
- const { labels = [], icons = [] } = legend;
-
- const Icons = labels.map((label, idx) => (
-
-
-
-
- {label}
-
- ));
-
- return (
-
- {Icons}
-
- );
-}
-
-LegendIcon.defaultProps = {
- legend: {
- labels: [],
- icons: []
- }
-};
-
-LegendIcon.propTypes = {
- legend: PropTypes.shape({
- labels: PropTypes.arrayOf(PropTypes.string),
- icons: PropTypes.arrayOf(PropTypes.string)
- }).isRequired
-};
-
-export default LegendIcon;
diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js
new file mode 100644
index 000000000..2b035644f
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/LegendLayer.js
@@ -0,0 +1,235 @@
+import React, { useMemo, useRef, useState } from 'react';
+import PropTypes from 'prop-types';
+import { Collapse, IconButton, Tooltip } from '@mui/material';
+import EyeIcon from '@mui/icons-material/VisibilityOutlined';
+import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import ExpandLessIcon from '@mui/icons-material/ExpandLess';
+import {
+ LayerVariablesList,
+ LegendItemHeader,
+ LegendLayerTitleWrapper,
+ LegendLayerWrapper
+} from './LegendWidgetUI.styles';
+import LegendOpacityControl from './LegendOpacityControl';
+import LegendLayerTitle from './LegendLayerTitle';
+import LegendLayerVariable from './LegendLayerVariable';
+import { useIntl } from 'react-intl';
+import useImperativeIntl from '../../hooks/useImperativeIntl';
+import Typography from '../../components/atoms/Typography';
+
+const EMPTY_OBJ = {};
+
+/**
+ * @param {import('./LegendWidgetUI').LegendLayerData['legend']} legend
+ * @returns {boolean}
+ */
+function isLegendEmpty(legend) {
+ if (!legend) {
+ return true;
+ }
+
+ if (Array.isArray(legend)) {
+ return legend.every((l) => isLegendEmpty(l));
+ }
+
+ return !legend.select && !legend.type;
+}
+
+/**
+ * Receives configuration options, send change events and renders a legend item
+ * @param {object} props
+ * @param {Object.} props.customLegendTypes - Allow to customise by default legend types that can be rendered.
+ * @param {import('./LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store.
+ * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change.
+ * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change.
+ * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change.
+ * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer selection change.
+ * @param {number} props.maxZoom - Global maximum zoom level for the map.
+ * @param {number} props.minZoom - Global minimum zoom level for the map.
+ * @param {number} props.currentZoom - Current zoom level for the map.
+ * @returns {React.ReactNode}
+ */
+export default function LegendLayer({
+ customLegendTypes = EMPTY_OBJ,
+ layer = EMPTY_OBJ,
+ onChangeCollapsed,
+ onChangeOpacity,
+ onChangeVisibility,
+ onChangeSelection,
+ maxZoom,
+ minZoom,
+ currentZoom
+}) {
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+ const menuAnchorRef = useRef(null);
+ const [opacityOpen, setOpacityOpen] = useState(false);
+
+ // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget
+ const id = layer.id;
+ const title = layer.title;
+ const visible = layer.visible ?? true;
+ const switchable = layer.switchable ?? true;
+ const collapsed = layer.collapsed ?? false;
+ const collapsible = (layer.collapsible ?? true) && !isLegendEmpty(layer.legend);
+ const opacity = layer.opacity ?? 1;
+ const showOpacityControl = layer.showOpacityControl ?? true;
+ const isExpanded = visible && !collapsed;
+ const collapseIcon = isExpanded ? : ;
+
+ const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined;
+ const showZoomNote =
+ layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom);
+ const outsideCurrentZoom = currentZoom < layer.minZoom || currentZoom > layer.maxZoom;
+
+ const zoomHelperText = getZoomHelperText({
+ intl: intlConfig,
+ minZoom,
+ maxZoom,
+ layerMinZoom: layer.minZoom,
+ layerMaxZoom: layer.maxZoom
+ });
+ const helperText = layer.helperText ?? (showZoomNote ? zoomHelperText : '');
+
+ const legendLayerVariables = useMemo(() => {
+ if (!layer.legend) {
+ return [];
+ }
+ return Array.isArray(layer.legend) ? layer.legend : [layer.legend];
+ }, [layer.legend]);
+
+ if (!layer.legend) {
+ return null;
+ }
+
+ return (
+
+
+ {collapsible && (
+ onChangeCollapsed({ id, collapsed: !collapsed })}
+ >
+ {collapseIcon}
+
+ )}
+
+
+ {showZoomNote && (
+
+
+ {intlConfig.formatMessage({ id: 'c4r.widgets.legend.zoomLevel' })}{' '}
+ {layer.minZoom} - {layer.maxZoom}
+
+
+ )}
+
+ {showOpacityControl && visible && !collapsed && (
+ onChangeOpacity({ id, opacity })}
+ />
+ )}
+ {switchable && (
+
+
+ onChangeVisibility({
+ id,
+ collapsed: visible ? collapsed : false,
+ visible: !visible
+ })
+ }
+ >
+ {visible ? : }
+
+
+ )}
+
+
+
+ {legendLayerVariables.map((legend, index) => (
+
+ onChangeSelection({ id, index, selection })
+ }
+ />
+ ))}
+
+ {helperText && (
+
+
+
+ )}
+
+
+ );
+}
+
+LegendLayer.propTypes = {
+ customLegendTypes: PropTypes.object.isRequired,
+ layer: PropTypes.object.isRequired,
+ onChangeCollapsed: PropTypes.func.isRequired,
+ onChangeOpacity: PropTypes.func.isRequired,
+ onChangeVisibility: PropTypes.func.isRequired,
+ onChangeSelection: PropTypes.func.isRequired,
+ maxZoom: PropTypes.number,
+ minZoom: PropTypes.number,
+ currentZoom: PropTypes.number
+};
+LegendLayer.defaultProps = {
+ maxZoom: 21,
+ minZoom: 0,
+ currentZoom: 0
+};
+
+/**
+ * @param {object} props
+ * @param {import('react-intl').IntlShape} props.intl - React Intl object.
+ * @param {number} props.minZoom - Global minimum zoom level for the map.
+ * @param {number} props.maxZoom - Global maximum zoom level for the map.
+ * @param {number} props.layerMinZoom - Layer minimum zoom level.
+ * @param {number} props.layerMaxZoom - Layer maximum zoom level.
+ * @returns {string}
+ */
+function getZoomHelperText({ intl, minZoom, maxZoom, layerMinZoom, layerMaxZoom }) {
+ const and = intl.formatMessage({ id: 'c4r.widgets.legend.and' });
+ const lowerThan = intl.formatMessage({ id: 'c4r.widgets.legend.lowerThan' });
+ const greaterThan = intl.formatMessage({ id: 'c4r.widgets.legend.greaterThan' });
+ const note = intl.formatMessage({ id: 'c4r.widgets.legend.zoomNote' });
+
+ const maxZoomText = layerMaxZoom < maxZoom ? `${lowerThan} ${layerMaxZoom}` : '';
+ const minZoomText = layerMinZoom > minZoom ? `${greaterThan} ${layerMinZoom}` : '';
+ const texts = [maxZoomText, minZoomText].filter(Boolean).join(` ${and} `);
+ return texts ? `${note} ${texts}` : '';
+}
diff --git a/packages/react-ui/src/widgets/legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/legend/LegendLayerTitle.js
new file mode 100644
index 000000000..a88b62fce
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/LegendLayerTitle.js
@@ -0,0 +1,43 @@
+import React, { useLayoutEffect, useRef, useState } from 'react';
+import { Tooltip } from '@mui/material';
+import Typography from '../../components/atoms/Typography';
+
+/** Renders the legend layer title with an optional tooltip if the title is detected to be too long.
+ * @param {object} props
+ * @param {string} props.title
+ * @param {boolean} props.visible
+ * @param {object} props.typographyProps
+ * @returns {React.ReactNode}
+ */
+export default function LegendLayerTitle({ title, visible, typographyProps }) {
+ const ref = useRef(null);
+ const [isOverflow, setIsOverflow] = useState(false);
+
+ useLayoutEffect(() => {
+ if (visible && ref.current) {
+ const { offsetWidth, scrollWidth } = ref.current;
+ setIsOverflow(offsetWidth < scrollWidth);
+ }
+ }, [title, visible]);
+
+ const element = (
+
+ {title}
+
+ );
+
+ if (!isOverflow) {
+ return element;
+ }
+
+ return {element};
+}
diff --git a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js
new file mode 100644
index 000000000..18f4ba1aa
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js
@@ -0,0 +1,129 @@
+import React from 'react';
+import { Box, ListItemText, MenuItem, Select } from '@mui/material';
+import LegendCategories from './legend-types/LegendCategories';
+import LegendIcon from './legend-types/LegendIcon';
+import LegendRamp from './legend-types/LegendRamp';
+import LegendProportion from './legend-types/LegendProportion';
+import LEGEND_TYPES from './legend-types/LegendTypes';
+import { useIntl } from 'react-intl';
+import useImperativeIntl from '../../hooks/useImperativeIntl';
+import Typography from '../../components/atoms/Typography';
+
+const legendTypeMap = {
+ [LEGEND_TYPES.CATEGORY]: LegendCategories,
+ [LEGEND_TYPES.PROPORTION]: LegendProportion,
+ [LEGEND_TYPES.ICON]: LegendIcon,
+ [LEGEND_TYPES.BINS]: (props) => ,
+ [LEGEND_TYPES.CONTINUOUS_RAMP]: (props) =>
+};
+
+/**
+ * @param {object} props
+ * @param {import('./LegendWidgetUI').LegendLayerVariableBase} props.legend - legend variable data.
+ * @returns {React.ReactNode}
+ */
+function LegendUnknown({ legend }) {
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+
+ if (legend.select || !legend.type) {
+ return null;
+ }
+
+ return (
+
+ {legend.type} {intlConfig.formatMessage({ id: 'c4r.widgets.legend.notSupported' })}.
+
+ );
+}
+
+/**
+ * @param {import('./LegendWidgetUI').LegendLayerVariableData} legend - legend variable data.
+ * @returns {string}
+ */
+function getLegendSubtitle(legend) {
+ if (legend.type === LEGEND_TYPES.PROPORTION) {
+ return 'c4r.widgets.legend.subtitles.proportion';
+ }
+ if (legend.type === LEGEND_TYPES.ICON || !!legend.customMarkers) {
+ return 'c4r.widgets.legend.subtitles.icon';
+ }
+ if (legend.isStrokeColor) {
+ return 'c4r.widgets.legend.subtitles.strokeColor';
+ }
+ return 'c4r.widgets.legend.subtitles.color';
+}
+
+/**
+ * @param {object} props
+ * @param {import('./LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store.
+ * @param {import('./LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data.
+ * @param {Object.} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered.
+ * @param {(selection: unknown) => void} props.onChangeSelection - Callback function for legend options change.
+ * @returns {React.ReactNode}
+ */
+export default function LegendLayerVariable({
+ layer,
+ legend,
+ customLegendTypes,
+ onChangeSelection
+}) {
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+
+ const type = legend.type;
+ const TypeComponent = legendTypeMap[type] || customLegendTypes[type] || LegendUnknown;
+ const selectOptions = legend.select?.options || [];
+
+ return (
+
+ {legend.attr ? (
+
+
+ {intlConfig.formatMessage({ id: getLegendSubtitle(legend) })}
+
+
+ {legend.attr}
+
+
+ ) : null}
+ {legend.select ? (
+
+
+ {legend.select.label}
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/packages/react-ui/src/widgets/legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js
new file mode 100644
index 000000000..c2b2a3ff7
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import { IconButton, InputAdornment, Popover, Slider, Tooltip } from '@mui/material';
+import { OpacityTextField, StyledOpacityControl } from './LegendWidgetUI.styles';
+import { useIntl } from 'react-intl';
+import useImperativeIntl from '../../hooks/useImperativeIntl';
+import OpacityIcon from '../../assets/icons/OpacityIcon';
+
+/**
+ * @param {object} props
+ * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor
+ * @param {boolean} props.open - Open state of the popover
+ * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change
+ * @param {number} props.opacity - Opacity value
+ * @param {(opacity: number) => void} props.onChange - Callback function for opacity change
+ * @returns {React.ReactNode}
+ */
+export default function LegendOpacityControl({
+ menuRef,
+ open,
+ toggleOpen,
+ opacity,
+ onChange
+}) {
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+
+ function handleTextFieldChange(e) {
+ const newOpacity = parseInt(e.target.value || '0');
+ const clamped = Math.min(Math.max(newOpacity, 0), 100);
+ onChange(clamped / 100);
+ }
+
+ return (
+ <>
+
+ toggleOpen(!open)}
+ >
+
+
+
+ toggleOpen(false)}
+ anchorEl={menuRef.current}
+ anchorOrigin={{
+ vertical: 'top',
+ horizontal: 'right'
+ }}
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'right'
+ }}
+ slotProps={{
+ root: {
+ sx: { transform: 'translate(-12px, 36px)' }
+ }
+ }}
+ >
+
+ onChange(v / 100)}
+ min={0}
+ max={100}
+ step={1}
+ />
+
+ {' '}
+ %
+
+ )
+ }}
+ />
+
+
+ >
+ );
+}
diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js
deleted file mode 100644
index a02bd80d6..000000000
--- a/packages/react-ui/src/widgets/legend/LegendProportion.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import { Box, Grid, styled } from '@mui/material';
-import PropTypes from 'prop-types';
-import Typography from '../../components/atoms/Typography';
-
-const sizes = {
- 0: 12,
- 1: 9,
- 2: 6,
- 3: 3
-};
-
-const Circle = styled(Box, {
- shouldForwardProp: (prop) => prop !== 'index'
-})(({ index = 0, theme }) => {
- const width = theme.spacing(sizes[index]);
- const height = theme.spacing(sizes[index]);
-
- return {
- border: `solid 1px ${theme.palette.grey[100]}`,
- backgroundColor: theme.palette.grey[50],
- borderRadius: '50%',
- position: 'absolute',
- right: 0,
- bottom: 0,
- width,
- height
- };
-});
-
-const ProportionalGrid = styled(Grid)(({ theme: { spacing } }) => ({
- justifyContent: 'flex-end',
- marginBottom: spacing(0.5),
- position: 'relative'
-}));
-
-function LegendProportion({ legend }) {
- const { min, max, error } = calculateRange(legend);
- const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0];
-
- return (
-
-
- {[...Array(4)].map((circle, index) => (
-
- ))}
-
-
- {error ? (
-
-
- You need to specify valid numbers for the labels property
-
-
- ) : (
- <>
-
- Max: {max}
-
-
- {step2}
-
-
- {step1}
-
-
- Min: {min}
-
- >
- )}
-
-
- );
-}
-
-LegendProportion.defaultProps = {
- legend: {
- labels: []
- }
-};
-
-LegendProportion.propTypes = {
- legend: PropTypes.shape({
- labels: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))
- }).isRequired
-};
-
-export default LegendProportion;
-
-// Aux
-export function getMinMax({ labels }) {
- let max = labels?.[labels.length - 1];
- let min = labels?.[0];
-
- if (!Number.isFinite(min)) {
- min = parseInt(min, 10);
- }
-
- if (!Number.isFinite(max)) {
- max = parseInt(max, 10);
- }
-
- return [min, max];
-}
-
-function calculateRange(legend) {
- let error = false;
- const [min, max] = getMinMax(legend);
-
- if (Number.isNaN(min) || Number.isNaN(max)) {
- error = true;
- }
-
- return { min, max, error };
-}
-
-function calculateSteps(min, max) {
- const gap = (max + min) / 4;
- const step1 = min + gap;
- const step2 = max - gap;
-
- return [step1, step2];
-}
diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts
index ed791401c..01aaf558f 100644
--- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts
+++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts
@@ -1,8 +1,96 @@
+import { SxProps } from '@mui/material';
+import { Theme } from '@mui/system';
+import type React from 'react'
+
export enum LEGEND_TYPES {
CATEGORY = 'category',
ICON = 'icon',
CONTINUOUS_RAMP = 'continuous_ramp',
BINS = 'bins',
PROPORTION = 'proportion',
- CUSTOM = 'custom'
}
+
+export type LegendWidgetUIProps = {
+ customLegendTypes?: Record;
+ layers?: LegendLayerData[];
+ collapsed?: boolean;
+ onChangeCollapsed?: (collapsed: boolean) => void;
+ onChangeLegendRowCollapsed?: ({ id, collapsed }: { id: string, collapsed: boolean }) => void;
+ onChangeOpacity?: ({ id, opacity }: { id: string, opacity: number }) => void
+ onChangeVisibility?: ({ id, visible }: { id: string, visible: boolean }) => void
+ onChangeSelection?: ({ id, index, selection }: { id: string, index: number, selection: unknown }) => void
+ title?: string
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
+ maxZoom?: number
+ minZoom?: number
+ currentZoom?: number
+ isMobile?: boolean
+ sx?: SxProps
+}
+
+declare const LegendWidgetUI: (props: LegendWidgetUIProps) => React.ReactNode;
+export default LegendWidgetUI;
+
+export type LegendLayerData = {
+ id: string;
+ title?: string;
+ visible?: boolean; // layer visibility state
+ switchable?: boolean; // layer visibility state can be toggled on/off
+ collapsed?: boolean; // layer collapsed state
+ collapsible?: boolean; // layer collapsed state can be toggled on/off
+ opacity?: number; // layer opacity percentage
+ showOpacityControl?: boolean; // layer opacity percentage can be modified
+ helperText?: React.ReactNode; // note to show below all legend items
+ minZoom?: number; // min zoom at which layer is displayed
+ maxZoom?: number; // max zoom at which layer is displayed
+ legend?: LegendLayerVariableData | LegendLayerVariableData[];
+};
+
+export type LegendLayerVariableBase = {
+ type: LEGEND_TYPES | string;
+ select: LegendSelectConfig
+ attr?: string; // subtitle to show below the legend item toggle when expanded
+}
+export type LegendLayerVariableData = LegendLayerVariableBase & LegendType;
+
+export type LegendType = LegendBins | LegendRamp | LegendIcons | LegendCategories | LegendProportion;
+
+export type LegendColors = string | string[] | number[][];
+export type LegendNumericLabels = number[] | { label: string; value: number }[];
+
+export type LegendBins = {
+ colors: LegendColors
+ labels: LegendNumericLabels
+}
+export type LegendRamp = {
+ colors: LegendColors
+ labels: LegendNumericLabels
+}
+export type LegendIcons = {
+ icons: string[]
+ labels: string[]
+}
+export type LegendCategories = {
+ colors: LegendColors
+ labels: string[] | number[]
+ isStrokeColor?: boolean
+ customMarkers?: string | string[]
+ maskedMarkers?: boolean
+}
+export type LegendProportion = {
+ labels: [number, number]
+}
+
+export type LegendSelectConfig = {
+ label: string;
+ value: T;
+ options: {
+ label: string;
+ value: T;
+ }[];
+};
+
+export type CustomLegendComponent = React.ComponentType<{
+ layer: LegendLayerData;
+ legend: LegendLayerVariableData;
+}>;
diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js
index 42211fcbb..b141679a4 100644
--- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js
+++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js
@@ -1,250 +1,157 @@
-import React, { createRef, Fragment } from 'react';
-import { Box, Button, Collapse, Divider, Grid, styled, SvgIcon } from '@mui/material';
-import LegendWrapper from './LegendWrapper';
-import LegendCategories from './LegendCategories';
-import LegendIcon from './LegendIcon';
-import LegendRamp from './LegendRamp';
-import LegendProportion from './LegendProportion';
+import React from 'react';
import PropTypes from 'prop-types';
+import { Collapse, Drawer, IconButton, Tooltip } from '@mui/material';
+import CloseIcon from '@mui/icons-material/Close';
+import LayerIcon from '@mui/icons-material/LayersOutlined';
+import { LegendContent, LegendRoot, LegendToggleHeader } from './LegendWidgetUI.styles';
+import LegendLayer from './LegendLayer';
+import { useIntl } from 'react-intl';
+import useImperativeIntl from '../../hooks/useImperativeIntl';
import Typography from '../../components/atoms/Typography';
-const LayersIcon = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const LegendBox = styled(Box)(({ theme }) => ({
- minWidth: theme.spacing(30),
- backgroundColor: theme.palette.background.paper,
- boxShadow: theme.shadows[1],
- borderRadius: theme.spacing(0.5)
-}));
-
+const EMPTY_OBJ = {};
+const EMPTY_FN = () => {};
+const EMPTY_ARR = [];
+
+/**
+ * @param {object} props
+ * @param {Object.} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered.
+ * @param {import('./LegendWidgetUI').LegendLayerData[]} [props.layers] - Array of layer objects from redux store.
+ * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget.
+ * @param {(collapsed: boolean) => void} [props.onChangeCollapsed] - Callback function for collapsed state change.
+ * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} [props.onChangeLegendRowCollapsed] - Callback function for layer visibility change.
+ * @param {({ id, opacity }: { id: string, opacity: number }) => void} [props.onChangeOpacity] - Callback function for layer opacity change.
+ * @param {({ id, visible }: { id: string, visible: boolean }) => void} [props.onChangeVisibility] - Callback function for layer collapsed state change.
+ * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer variable selection change.
+ * @param {string} [props.title] - Title of the toggle button when widget is open.
+ * @param {number} [props.maxZoom] - Global maximum zoom level for the map.
+ * @param {number} [props.minZoom] - Global minimum zoom level for the map.
+ * @param {number} [props.currentZoom] - Current zoom level for the map.
+ * @param {boolean} [props.isMobile] - Whether the widget is displayed on a mobile device.
+ * @param {import('@mui/system').SxProps} [props.sx] - Style object for the root component.
+ * @returns {React.ReactNode}
+ */
function LegendWidgetUI({
- customLegendTypes,
- customLayerOptions,
- layers = [],
- collapsed,
- onChangeCollapsed,
- onChangeVisibility,
- onChangeOpacity,
- onChangeLegendRowCollapsed,
- title
-}) {
- const isSingle = layers.length === 1;
+ customLegendTypes = EMPTY_OBJ,
+ layers = EMPTY_ARR,
+ collapsed = false,
+ onChangeCollapsed = EMPTY_FN,
+ onChangeVisibility = EMPTY_FN,
+ onChangeOpacity = EMPTY_FN,
+ onChangeLegendRowCollapsed = EMPTY_FN,
+ onChangeSelection = EMPTY_FN,
+ title,
+ maxZoom = 20,
+ minZoom = 0,
+ currentZoom,
+ isMobile,
+ sx
+} = {}) {
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+
+ const legendToggleHeader = (
+
+
+ {title}
+
+
+ onChangeCollapsed(true)}>
+
+
+
+
+ );
+ const legendToggleButton = (
+
+ onChangeCollapsed(false)}>
+
+
+
+ );
return (
-
-
-
-
-
+
+ {isMobile ? (
+ <>
+ {legendToggleButton}
+ onChangeCollapsed(true)}
+ >
+ {legendToggleHeader}
+
+ {layers.map((l) => (
+
+ ))}
+
+
+ >
+ ) : (
+ <>
+ {collapsed ? legendToggleButton : legendToggleHeader}
+
+
+ {layers.map((l) => (
+
+ ))}
+
+
+ >
+ )}
+
);
}
LegendWidgetUI.defaultProps = {
- layers: [],
- customLegendTypes: {},
- customLayerOptions: {},
+ layers: EMPTY_ARR,
+ customLegendTypes: EMPTY_OBJ,
collapsed: false,
- title: 'Layers'
+ title: 'Layers',
+ onChangeCollapsed: EMPTY_FN,
+ onChangeLegendRowCollapsed: EMPTY_FN,
+ onChangeVisibility: EMPTY_FN,
+ onChangeOpacity: EMPTY_FN,
+ onChangeSelection: EMPTY_FN
};
LegendWidgetUI.propTypes = {
customLegendTypes: PropTypes.objectOf(PropTypes.func),
- customLayerOptions: PropTypes.objectOf(PropTypes.func),
layers: PropTypes.array,
collapsed: PropTypes.bool,
onChangeCollapsed: PropTypes.func,
onChangeLegendRowCollapsed: PropTypes.func,
onChangeVisibility: PropTypes.func,
onChangeOpacity: PropTypes.func,
- title: PropTypes.string
+ onChangeSelection: PropTypes.func,
+ title: PropTypes.string,
+ maxZoom: PropTypes.number,
+ minZoom: PropTypes.number,
+ currentZoom: PropTypes.number,
+ isMobile: PropTypes.bool
};
export default LegendWidgetUI;
-
-const Header = styled(Grid)(({ theme }) => ({
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- height: theme.spacing(4.5)
-}));
-
-const HeaderButton = styled(Button, {
- shouldForwardProp: (prop) => prop !== 'collapsed'
-})(({ theme, collapsed }) => ({
- flex: '1 1 auto',
- justifyContent: 'space-between',
- padding: theme.spacing(0.75, 1.25, 0.75, 3),
- borderTop: collapsed ? 'none' : `1px solid ${theme.palette.divider}`,
- cursor: 'pointer'
-}));
-
-const CollapseWrapper = styled(Collapse)(() => ({
- '.MuiCollapse-wrapperInner': {
- maxHeight: 'max(350px, 80vh)',
- overflowY: 'auto',
- overflowX: 'hidden'
- }
-}));
-
-function LegendContainer({ isSingle, children, collapsed, onChangeCollapsed, title }) {
- const wrapper = createRef();
-
- const handleExpandClick = () => {
- if (onChangeCollapsed) onChangeCollapsed(!collapsed);
- };
-
- return isSingle ? (
- children
- ) : (
- <>
-
- {children}
-
-
- }
- onClick={handleExpandClick}
- >
- {title}
-
-
- >
- );
-}
-
-export const LEGEND_TYPES = {
- CATEGORY: 'category',
- ICON: 'icon',
- CONTINUOUS_RAMP: 'continuous_ramp',
- BINS: 'bins',
- PROPORTION: 'proportion',
- CUSTOM: 'custom'
-};
-
-const LEGEND_COMPONENT_BY_TYPE = {
- [LEGEND_TYPES.CATEGORY]: LegendCategories,
- [LEGEND_TYPES.ICON]: LegendIcon,
- [LEGEND_TYPES.CONTINUOUS_RAMP]: (args) => ,
- [LEGEND_TYPES.BINS]: (args) => ,
- [LEGEND_TYPES.PROPORTION]: LegendProportion,
- [LEGEND_TYPES.CUSTOM]: ({ legend }) => legend.children
-};
-
-function UnknownLayerOption({ optionKey }) {
- return (
-
- Unknown layer option{' '}
-
- {optionKey}
-
-
- );
-}
-
-function LegendRows({
- layers = [],
- customLegendTypes,
- customLayerOptions,
- onChangeVisibility,
- onChangeOpacity,
- onChangeCollapsed
-}) {
- const isSingle = layers.length === 1;
-
- return (
- <>
- {layers.map((layer, index) => {
- const {
- id,
- title,
- switchable,
- visible,
- options = [],
- showOpacityControl = false,
- opacity = 1,
- legend = {}
- } = layer;
-
- const {
- type = LEGEND_TYPES.CUSTOM,
- collapsible = true,
- collapsed = false,
- note = '',
- attr = '',
- children
- } = legend;
-
- const isLast = layers.length - 1 === index;
- const LegendComponent =
- LEGEND_COMPONENT_BY_TYPE[type] || customLegendTypes[type] || UnknownLegend;
- const hasChildren = type === LEGEND_TYPES.CUSTOM ? !!children : !!LegendComponent;
-
- const layerOptions = options.map((key) => {
- const Component = customLayerOptions[key] || UnknownLayerOption;
- return ;
- });
-
- return (
-
-
-
-
- {!isSingle && !isLast && }
-
- );
- })}
- >
- );
-}
-
-function UnknownLegend({ legend }) {
- return (
- {legend.type} is not a known legend type.
- );
-}
diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js
new file mode 100644
index 000000000..1e78933d4
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js
@@ -0,0 +1,112 @@
+import { Box, Paper, TextField, styled } from '@mui/material';
+import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants';
+
+export const LEGEND_WIDTH = 240;
+
+export const LegendRoot = styled(Paper, {
+ shouldForwardProp: (prop) => !['collapsed'].includes(prop)
+})(({ theme, collapsed }) => ({
+ width: collapsed ? 'min-content' : LEGEND_WIDTH,
+ background: theme.palette.background.paper,
+ maxHeight: 'calc(100% - 120px)',
+ display: 'flex',
+ flexDirection: 'column'
+}));
+
+export const LegendToggleHeader = styled('header', {
+ shouldForwardProp: (prop) => !['collapsed'].includes(prop)
+})(({ theme, collapsed }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: theme.spacing(1),
+ paddingLeft: theme.spacing(2),
+ borderBottom: collapsed ? undefined : `1px solid ${theme.palette.divider}`
+}));
+
+export const LegendItemHeader = styled('header')(({ theme }) => ({
+ padding: theme.spacing(1.5),
+ paddingRight: theme.spacing(2),
+ gap: theme.spacing(0.5),
+ display: 'flex',
+ justifyContent: 'space-between',
+ position: 'sticky',
+ zIndex: 2,
+ top: 0,
+ background: theme.palette.background.paper
+}));
+
+export const StyledOpacityControl = styled('div')(({ theme }) => ({
+ display: 'flex',
+ gap: theme.spacing(2),
+ alignItems: 'center',
+ padding: theme.spacing(1),
+ minWidth: LEGEND_WIDTH - theme.spacingValue * 4
+}));
+
+export const OpacityTextField = styled(TextField)(({ theme }) => ({
+ display: 'flex',
+ width: theme.spacing(7.5),
+ flexShrink: 0,
+ 'input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button':
+ {
+ '-webkit-appearance': 'none',
+ margin: 0
+ }
+}));
+
+export const LayerVariablesList = styled('ul', {
+ shouldForwardProp: (prop) => !['opacity'].includes(prop)
+})(({ theme, opacity }) => ({
+ opacity,
+ margin: 0,
+ padding: 0,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1)
+}));
+
+export const LegendVariableList = styled('ul')(({ theme }) => ({
+ margin: 0,
+ padding: 0,
+ paddingBottom: theme.spacing(1),
+ display: 'flex',
+ flexDirection: 'column'
+}));
+
+export const LegendIconWrapper = styled('li')(() => ({
+ display: 'flex',
+ alignItems: 'center'
+}));
+
+export const LegendIconImageWrapper = styled('div')(({ theme }) => ({
+ marginRight: theme.spacing(1.5),
+ width: ICON_SIZE_MEDIUM,
+ height: ICON_SIZE_MEDIUM,
+ '& img': {
+ margin: 'auto',
+ display: 'block'
+ }
+}));
+
+export const LegendContent = styled(Box, {
+ shouldForwardProp: (prop) => !['width'].includes(prop)
+})(({ width, theme }) => ({
+ width,
+ overflow: 'auto',
+ maxHeight: `calc(100% - ${theme.spacing(1.5)})`
+}));
+
+export const LegendLayerWrapper = styled('section')(({ theme }) => ({
+ '&:not(:first-of-type)': {
+ borderTop: `1px solid ${theme.palette.divider}`
+ }
+}));
+
+export const LegendLayerTitleWrapper = styled('div')(() => ({
+ flexGrow: 1,
+ flexShrink: 1,
+ minWidth: 0
+}));
+
+export const styles = {};
diff --git a/packages/react-ui/src/widgets/legend/LegendWrapper.js b/packages/react-ui/src/widgets/legend/LegendWrapper.js
deleted file mode 100644
index 766b28326..000000000
--- a/packages/react-ui/src/widgets/legend/LegendWrapper.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import React, { createRef, useState } from 'react';
-import { ExpandLess, ExpandMore } from '@mui/icons-material';
-import { useIntl } from 'react-intl';
-import {
- Box,
- Button,
- Collapse,
- Grid,
- Icon,
- Switch,
- ToggleButton,
- Tooltip,
- styled
-} from '@mui/material';
-import LayerIcon from '../../assets/icons/LayerIcon';
-import Typography from '../../components/atoms/Typography';
-import OpacityControl from '../OpacityControl';
-import Note from './Note';
-import useImperativeIntl from '../../hooks/useImperativeIntl';
-
-const Wrapper = styled(Box)(() => ({
- position: 'relative',
- maxWidth: '100%',
- padding: 0
-}));
-
-const LayerOptionsWrapper = styled(Box)(({ theme }) => ({
- backgroundColor: theme.palette.grey[50],
- marginTop: theme.spacing(2)
-}));
-
-function LegendWrapper({
- id,
- title,
- layerOptions,
- switchable = true,
- collapsible = true,
- collapsed = false,
- visible = true,
- hasChildren = true,
- note,
- attr,
- children,
- showOpacityControl,
- opacity,
- onChangeOpacity,
- onChangeVisibility,
- onChangeCollapsed
-}) {
- const wrapper = createRef();
- const expanded = !collapsed;
- const [isLayerOptionsExpanded, setIsLayerOptionsExpanded] = useState(false);
-
- const intl = useIntl();
- const intlConfig = useImperativeIntl(intl);
-
- const handleChangeOpacity = (newOpacity) => {
- if (onChangeOpacity) onChangeOpacity({ id, opacity: newOpacity });
- };
-
- const handleExpandClick = () => {
- if (collapsible && onChangeCollapsed)
- onChangeCollapsed({ id, collapsed: !collapsed });
- };
-
- const handleChangeVisibility = () => {
- if (onChangeVisibility) onChangeVisibility({ id, visible: !visible });
- };
-
- const handleToggleLayerOptions = () => {
- setIsLayerOptionsExpanded((oldState) => !oldState);
- };
-
- return (
-
- 0}
- onToggleLayerOptions={handleToggleLayerOptions}
- isLayerOptionsExpanded={isLayerOptionsExpanded}
- intl={intlConfig}
- />
- {hasChildren && !!children && (
-
-
-
- {attr && (
-
- {intlConfig.formatMessage({ id: 'c4r.widgets.legend.by' }, { attr })}
-
- )}
- {children}
-
-
- {showOpacityControl && (
-
- )}
- {layerOptions}
-
-
- {note}
-
-
-
- )}
-
- );
-}
-
-const GridHeader = styled(Grid)(({ theme }) => ({
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- height: '60px',
- padding: theme.spacing(1.25, 1.25, 1.25, 2.5)
-}));
-
-const ButtonHeader = styled(Button, {
- shouldForwardProp: (prop) => prop !== 'collapsible'
-})(({ theme, collapsible }) => ({
- padding: 0,
- flex: '1 1 auto',
- justifyContent: 'flex-start',
- cursor: collapsible ? 'pointer' : 'default',
- '& .MuiButton-startIcon': {
- marginRight: theme.spacing(1)
- },
- '&:hover': {
- background: 'none'
- }
-}));
-
-const ParentIcon = ({ theme }) => ({
- display: 'block',
- fill: theme.palette.text.secondary
-});
-
-const MoreIconHeader = styled(ExpandMore)(({ theme }) => ParentIcon({ theme }));
-const LessIconHeader = styled(ExpandLess)(({ theme }) => ParentIcon({ theme }));
-
-function Header({
- title,
- switchable,
- visible,
- collapsible,
- expanded,
- onExpandClick,
- onChangeVisibility,
- layerOptionsEnabled,
- onToggleLayerOptions,
- isLayerOptionsExpanded,
- intl
-}) {
- const ExpandIcon = expanded ? LessIconHeader : MoreIconHeader;
-
- return (
-
-
-
-
- )
- }
- onClick={onExpandClick}
- >
- {title}
-
- {!!layerOptionsEnabled && (
-
-
-
-
-
- )}
- {switchable && (
-
-
-
- )}
-
- );
-}
-
-export default LegendWrapper;
diff --git a/packages/react-ui/src/widgets/legend/Note.js b/packages/react-ui/src/widgets/legend/Note.js
deleted file mode 100644
index b2e44d74e..000000000
--- a/packages/react-ui/src/widgets/legend/Note.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Box } from '@mui/material';
-import React from 'react';
-import Typography from '../../components/atoms/Typography';
-
-export default function Note({ children }) {
- if (!children) {
- return null;
- }
-
- return (
-
- Note:{' '}
- {children}
-
- );
-}
diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js
new file mode 100644
index 000000000..8a82c7633
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import { Box, styled } from '@mui/material';
+import { getPalette } from '../../../utils/palette';
+import PropTypes from 'prop-types';
+import LegendLayerTitle from '../LegendLayerTitle';
+import { LegendVariableList } from '../LegendWidgetUI.styles';
+import useImperativeIntl from '../../../hooks/useImperativeIntl';
+import { useIntl } from 'react-intl';
+import Typography from '../../../components/atoms/Typography';
+
+const MAX_CATEGORIES = 20;
+
+/**
+ * @param {object} props
+ * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendCategories} props.legend - legend variable data.
+ * @returns {React.ReactNode}
+ */
+function LegendCategories({ legend }) {
+ const {
+ labels = [],
+ colors = [],
+ isStrokeColor = false,
+ customMarkers,
+ maskedMarkers = true
+ } = legend;
+
+ const palette = getPalette(colors, labels.length);
+ const showHelperText = labels.length > MAX_CATEGORIES;
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+
+ return (
+ <>
+
+ {labels.slice(0, MAX_CATEGORIES).map((label, idx) => (
+
+ ))}
+
+ {showHelperText && (
+
+ {intlConfig.formatMessage(
+ {
+ id: 'c4r.widgets.legend.maxCategories'
+ },
+ {
+ n: MAX_CATEGORIES
+ }
+ )}
+
+ )}
+ >
+ );
+}
+
+LegendCategories.defaultProps = {
+ legend: {
+ labels: [],
+ colors: [],
+ isStrokeColor: false
+ }
+};
+
+const ColorType = PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.number)
+]);
+
+LegendCategories.propTypes = {
+ legend: PropTypes.shape({
+ labels: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
+ colors: PropTypes.oneOfType([PropTypes.arrayOf(ColorType), PropTypes.string]),
+ customMarkers: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.string),
+ PropTypes.string
+ ]),
+ maskedMarkers: PropTypes.bool,
+ isStrokeColor: PropTypes.bool
+ }).isRequired
+};
+
+export default LegendCategories;
+
+const getCircleStyles = ({ isMax, color, isStrokeColor, theme }) => ({
+ border: '2px solid transparent',
+ '&::after': {
+ position: 'absolute',
+ display: isMax ? 'block' : 'none',
+ content: '""',
+ width: theme.spacing(2),
+ height: theme.spacing(2),
+ border: `2px solid ${theme.palette.grey[900]}`,
+ transform: 'translate(-30%, -30%)',
+ borderRadius: '50%',
+ boxSizing: 'content-box'
+ },
+ ...(isStrokeColor ? { borderColor: color } : { backgroundColor: color })
+});
+
+const getIconStyles = ({ icon, color, maskedIcon }) => ({
+ maskRepeat: 'no-repeat',
+ maskSize: 'cover',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: 'cover',
+ ...(maskedIcon
+ ? {
+ backgroundColor: color,
+ maskImage: `url(${icon})`,
+ WebkitMaskImage: `url(${icon})`
+ }
+ : {
+ backgroundColor: `rgba(0,0,0,0)`,
+ backgroundImage: `url(${icon})`
+ })
+});
+
+const Marker = styled(Box, {
+ shouldForwardProp: (prop) =>
+ !['icon', 'maskedIcon', 'color', 'isStrokeColor'].includes(prop)
+})(({ icon, maskedIcon, color, isStrokeColor, theme }) => ({
+ whiteSpace: 'nowrap',
+ display: 'block',
+ width: theme.spacing(1.5),
+ height: theme.spacing(1.5),
+ borderRadius: '50%',
+ position: 'relative',
+ border: '2px solid transparent',
+ ...(icon
+ ? getIconStyles({ icon, color, maskedIcon })
+ : getCircleStyles({ color, isStrokeColor, theme }))
+}));
+
+function LegendCategoriesRow({ label, isStrokeColor, color = '#000', icon, maskedIcon }) {
+ return (
+
+
+
+
+ );
+}
diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js
new file mode 100644
index 000000000..3a9317bbe
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ICON_SIZE_MEDIUM } from '../../../theme/themeConstants';
+import {
+ LegendIconImageWrapper,
+ LegendIconWrapper,
+ LegendVariableList
+} from '../LegendWidgetUI.styles';
+import LegendLayerTitle from '../LegendLayerTitle';
+
+/**
+ * @param {object} props
+ * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendIcons} props.legend - legend variable data.
+ * @returns {React.ReactNode}
+ */
+function LegendIcon({ legend }) {
+ const { labels = [], icons = [] } = legend;
+ return (
+
+ {labels.map((label, idx) => (
+
+
+
+
+
+
+ ))}
+
+ );
+}
+
+LegendIcon.defaultProps = {
+ legend: {
+ labels: [],
+ icons: []
+ }
+};
+
+LegendIcon.propTypes = {
+ legend: PropTypes.shape({
+ labels: PropTypes.arrayOf(PropTypes.string),
+ icons: PropTypes.arrayOf(PropTypes.string)
+ }).isRequired
+};
+
+export default LegendIcon;
diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js b/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js
new file mode 100644
index 000000000..16b6fa8fd
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js
@@ -0,0 +1,158 @@
+import React from 'react';
+import { Box, styled } from '@mui/material';
+import PropTypes from 'prop-types';
+import Typography from '../../../components/atoms/Typography';
+import { useIntl } from 'react-intl';
+import useImperativeIntl from '../../../hooks/useImperativeIntl';
+
+const sizes = {
+ 0: 12,
+ 1: 9,
+ 2: 6,
+ 3: 3
+};
+
+const Circle = styled(Box, {
+ shouldForwardProp: (prop) => prop !== 'index'
+})(({ index = 0, theme }) => {
+ const width = theme.spacing(sizes[index]);
+ const height = theme.spacing(sizes[index]);
+
+ return {
+ border: `solid 1px ${theme.palette.divider}`,
+ backgroundColor: theme.palette.background.default,
+ borderRadius: '50%',
+ position: 'absolute',
+ right: 0,
+ bottom: 0,
+ width,
+ height
+ };
+});
+
+const CircleGrid = styled(Box)(({ theme: { spacing } }) => ({
+ display: 'flex',
+ justifyContent: 'flex-end',
+ flexShrink: 0,
+ position: 'relative',
+ width: spacing(sizes[0]),
+ height: spacing(sizes[0])
+}));
+
+const LegendProportionWrapper = styled(Box)(({ theme: { spacing } }) => ({
+ display: 'flex',
+ gap: spacing(1),
+ alignItems: 'stretch',
+ justifyContent: 'stretch',
+ padding: spacing(2, 0)
+}));
+
+const LabelList = styled(Box)(() => ({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-around',
+ gap: 1,
+ flexGrow: 1,
+ flexShrink: 1
+}));
+
+/**
+ * @param {object} props
+ * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendProportion} props.legend - legend variable data.
+ * @returns {React.ReactNode}
+ */
+function LegendProportion({ legend }) {
+ const intl = useIntl();
+ const intlConfig = useImperativeIntl(intl);
+
+ const showMinMax = legend.showMinMax ?? true;
+ const { min, max, error } = calculateRange(legend);
+ const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0];
+
+ return (
+
+
+ {[...Array(4)].map((_, index) => (
+
+ ))}
+
+
+ {error ? (
+
+
+ You need to specify valid numbers for the labels property
+
+
+ ) : (
+ <>
+
+ {showMinMax
+ ? `${intlConfig.formatMessage({ id: 'c4r.widgets.legend.max' })}: ${max}`
+ : max}
+
+
+ {step2}
+
+
+ {step1}
+
+
+ {showMinMax
+ ? `${intlConfig.formatMessage({ id: 'c4r.widgets.legend.min' })}: ${min}`
+ : min}
+
+ >
+ )}
+
+
+ );
+}
+
+LegendProportion.defaultProps = {
+ legend: {
+ labels: []
+ }
+};
+
+LegendProportion.propTypes = {
+ legend: PropTypes.shape({
+ labels: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))
+ }).isRequired
+};
+
+export default LegendProportion;
+
+// Aux
+export function getMinMax({ labels }) {
+ let max = labels?.[labels.length - 1];
+ let min = labels?.[0];
+
+ if (!Number.isFinite(min)) {
+ min = parseInt(min, 10);
+ }
+
+ if (!Number.isFinite(max)) {
+ max = parseInt(max, 10);
+ }
+
+ return [min, max];
+}
+
+function calculateRange(legend) {
+ let error = false;
+ const [min, max] = getMinMax(legend);
+
+ if (Number.isNaN(min) || Number.isNaN(max)) {
+ error = true;
+ }
+
+ return { min, max, error };
+}
+
+function calculateSteps(min, max) {
+ const gap = (max + min) / 4;
+ const step1 = min + gap;
+ const step2 = max - gap;
+
+ return [step1, step2];
+}
diff --git a/packages/react-ui/src/widgets/legend/LegendRamp.js b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js
similarity index 67%
rename from packages/react-ui/src/widgets/legend/LegendRamp.js
rename to packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js
index 11f7f5d14..8f2f5a1bf 100644
--- a/packages/react-ui/src/widgets/legend/LegendRamp.js
+++ b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js
@@ -1,12 +1,18 @@
-import { Grid, Tooltip, styled } from '@mui/material';
+import { Box, Tooltip, styled } from '@mui/material';
import PropTypes from 'prop-types';
import React from 'react';
-import Typography from '../../components/atoms/Typography';
-import { getPalette } from '../../utils/palette';
+import Typography from '../../../components/atoms/Typography';
+import { getPalette } from '../../../utils/palette';
import LegendProportion, { getMinMax } from './LegendProportion';
+/**
+ * @param {object} props
+ * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendRamp} props.legend - legend variable data.
+ * @param {boolean} [props.isContinuous] - If the legend is continuous.
+ * @returns {React.ReactNode}
+ */
function LegendRamp({ isContinuous = false, legend }) {
- const { labels = [], colors = [] } = legend;
+ const { labels = [], colors = [], showMinMax = true } = legend;
const palette = getPalette(
colors,
@@ -26,24 +32,24 @@ function LegendRamp({ isContinuous = false, legend }) {
let maxLabel = formattedLabels[formattedLabels.length - 1];
let minLabel = formattedLabels[0];
- if (!isContinuous) {
+ if (!isContinuous && showMinMax) {
minLabel = '< ' + minLabel;
maxLabel = '≥ ' + maxLabel;
}
return (
-
+
{error ? (
-
+
You need to specify valid numbers for the labels property
-
+
) : (
<>
-
+
{isContinuous ? (
-
+
) : (
)}
-
-
- {minLabel}
- {maxLabel}
-
+
+
+
+ {minLabel}
+
+
+ {maxLabel}
+
+
>
)}
-
+
);
}
@@ -86,17 +96,21 @@ LegendRamp.propTypes = {
export default LegendRamp;
-const StepsContinuous = styled(Grid, {
+const StepsContinuous = styled(Box, {
shouldForwardProp: (prop) => prop !== 'palette'
})(({ palette, theme }) => ({
+ display: 'block',
+ flexGrow: 1,
height: theme.spacing(1),
borderRadius: theme.spacing(0.5),
background: `linear-gradient(to right, ${palette.join()})`
}));
-const StepGrid = styled(Grid, {
+const StepGrid = styled(Box, {
shouldForwardProp: (prop) => prop !== 'color'
})(({ color, theme }) => ({
+ display: 'block',
+ flexGrow: 1,
height: theme.spacing(1),
backgroundColor: color,
'&:first-of-type': {
@@ -122,7 +136,7 @@ function StepsDiscontinuous({ labels = [], palette = [], max, min }) {
return (
-
+
);
})}
diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendTypes.js b/packages/react-ui/src/widgets/legend/legend-types/LegendTypes.js
new file mode 100644
index 000000000..4ee408770
--- /dev/null
+++ b/packages/react-ui/src/widgets/legend/legend-types/LegendTypes.js
@@ -0,0 +1,8 @@
+const LEGEND_TYPES = {
+ CATEGORY: 'category',
+ ICON: 'icon',
+ CONTINUOUS_RAMP: 'continuous_ramp',
+ BINS: 'bins',
+ PROPORTION: 'proportion'
+};
+export default LEGEND_TYPES;
diff --git a/packages/react-ui/storybook/.storybook/preview.js b/packages/react-ui/storybook/.storybook/preview.js
index 93888c561..9e26a36f7 100644
--- a/packages/react-ui/storybook/.storybook/preview.js
+++ b/packages/react-ui/storybook/.storybook/preview.js
@@ -1,14 +1,17 @@
-import React from 'react';
+import React, { useContext } from 'react';
import { withDesign } from 'storybook-addon-designs';
-import {
- createTheme,
- responsiveFontSizes,
- ThemeProvider,
- StyledEngineProvider,
- CssBaseline
-} from '@mui/material';
-import { cartoThemeOptions, theme } from '../../src/theme/carto-theme';
+import { ThemeProvider, StyledEngineProvider, CssBaseline } from '@mui/material';
+import { theme } from '../../src/theme/carto-theme';
import { BREAKPOINTS } from '../../src/theme/themeConstants';
+import {
+ Title,
+ Subtitle,
+ Primary,
+ ArgsTable,
+ Stories,
+ PRIMARY_STORY,
+ DocsContext
+} from '@storybook/addon-docs';
const customViewports = {
xs: {
@@ -89,13 +92,34 @@ export const decorators = [
)
];
+function CustomDescription() {
+ const context = useContext(DocsContext);
+ try {
+ const jsdoc = context.parameters.docs.extractComponentDescription(context.component);
+ const excerpt = jsdoc?.split('@param')[0];
+ return excerpt || null;
+ } catch {
+ return null;
+ }
+}
+
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
viewMode: 'docs',
docs: {
source: {
type: 'code'
- }
+ },
+ page: () => (
+ <>
+
+
+
+
+
+
+ >
+ )
},
options: {
storySort: {
diff --git a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js
index 51c9ae469..a9743bc00 100644
--- a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js
+++ b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js
@@ -1,21 +1,23 @@
-import React, { useState } from 'react';
+import { useReducer, useState } from 'react';
import LegendWidgetUI from '../../../src/widgets/legend/LegendWidgetUI';
import { IntlProvider } from 'react-intl';
+import { Box } from '@mui/material';
+import { fixtures } from './legendFixtures';
const options = {
title: 'Widgets/LegendWidgetUI',
component: LegendWidgetUI,
argTypes: {
- layers: {
- defaultValue: [],
+ collapsed: {
+ defaultValue: true,
control: {
- type: 'array'
+ type: 'boolean'
}
},
- collapsed: {
- defaultValue: false,
+ layers: {
+ defaultValue: fixtures,
control: {
- type: 'boolean'
+ type: 'array'
}
}
},
@@ -29,19 +31,61 @@ const options = {
};
export default options;
-const Widget = (props) => (
+/**
+ * @param {Parameters[0] & { height: number }} args
+ */
+const Widget = ({ height, ...props }) => (
-
+
+
+
);
+function useLegendState(args) {
+ const [collapsed, setCollapsed] = useState(args.collapsed);
+ const [layers, dispatch] = useReducer((state, action) => {
+ switch (action.type) {
+ case 'add':
+ return [...state, action.layer];
+ case 'remove':
+ return state.filter((layer) => layer.id !== action.layer.id);
+ case 'update':
+ return state.map((layer) => {
+ if (layer.id === action.layer.id) {
+ return { ...layer, ...action.layer };
+ }
+ return layer;
+ });
+ default:
+ throw new Error(`Unknown action type: ${action.type}`);
+ }
+ }, args.layers);
+
+ return { collapsed, setCollapsed, layers, dispatch };
+}
+
+/**
+ * @param {Parameters[0]} args
+ */
const Template = ({ ...args }) => {
+ const { collapsed, setCollapsed, layers, dispatch } = useLegendState(args);
+
return (
-
- Your Content
-
+ dispatch({ type: 'update', layer })}
+ onChangeOpacity={(layer) => dispatch({ type: 'update', layer })}
+ onChangeVisibility={(layer) => dispatch({ type: 'update', layer })}
+ currentZoom={13}
+ />
);
};
+export const Playground = Template.bind({});
const LegendTemplate = () => {
const layers = [
@@ -50,11 +94,13 @@ const LegendTemplate = () => {
title: 'Single Layer',
visible: true,
legend: {
- children: Your Content
+ type: 'category',
+ labels: ['Category 1', 'Category 2', 'Category 3'],
+ colors: ['#000', '#00F', '#0F0']
}
}
];
- return ;
+ return ;
};
const LegendNotFoundTemplate = () => {
@@ -62,49 +108,58 @@ const LegendNotFoundTemplate = () => {
{
id: 0,
title: 'Single Layer',
- visible: true
+ visible: true,
+ legend: {
+ type: 'unknown'
+ }
}
];
- return ;
+ return ;
};
-const LegendWithOpacityTemplate = () => {
+const LegendWithoutOpacityTemplate = () => {
const layers = [
{
id: 0,
title: 'Single Layer',
visible: true,
- showOpacityControl: true,
+ showOpacityControl: false,
opacity: 0.5,
legend: {
- children: Your Content
+ type: 'category',
+ labels: ['Category 1'],
+ colors: ['#faa']
}
}
];
- return ;
+ return ;
};
const LegendMultiTemplate = () => {
const layers = [
{
id: 0,
- title: 'Multi Layer',
+ title: 'Layer 1',
visible: true,
legend: {
- children: Your Content
+ type: 'category',
+ labels: ['Category 1'],
+ colors: ['#faa']
}
},
{
id: 1,
- title: 'Multi Layer',
+ title: 'Layer 2',
visible: false,
collapsible: false,
legend: {
- children: Your Content
+ type: 'category',
+ labels: ['Category 2'],
+ colors: ['#faf']
}
}
];
- return ;
+ return ;
};
const LegendMultiTemplateCollapsed = () => {
@@ -113,25 +168,29 @@ const LegendMultiTemplateCollapsed = () => {
const layers = [
{
id: 0,
- title: 'Multi Layer',
+ title: 'Layer 1',
visible: true,
legend: {
- children: Your Content
+ type: 'category',
+ labels: ['Category 1'],
+ colors: ['#faa']
}
},
{
id: 1,
- title: 'Multi Layer',
+ title: 'Layer 2',
visible: false,
collapsible: false,
legend: {
- children: Your Content
+ type: 'category',
+ labels: ['Category 2'],
+ colors: ['#faf']
}
}
];
return (
- setCollapsed(collapsed)}
@@ -142,8 +201,8 @@ const LegendMultiTemplateCollapsed = () => {
const categoryLegend = {
type: 'category',
note: 'lorem',
- colors: ['#000', '#00F', '#0F0'],
- labels: ['Category 1', 'Category 2', 'Category 3']
+ colors: 'RedOr', //['#000', '#00F', '#0F0'],
+ labels: Array.from({ length: 30 }, (_, i) => `Category ${i + 1}`)
};
const LegendCategoriesTemplate = () => {
@@ -155,7 +214,7 @@ const LegendCategoriesTemplate = () => {
legend: categoryLegend
}
];
- return ;
+ return ;
};
const LegendCategoriesStrokeTemplate = () => {
@@ -170,9 +229,10 @@ const LegendCategoriesStrokeTemplate = () => {
}
}
];
- return ;
+ return ;
};
+const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`;
const LegendIconTemplate = () => {
const layers = [
{
@@ -182,35 +242,29 @@ const LegendIconTemplate = () => {
legend: {
type: 'icon',
labels: ['Icon 1', 'Icon 2', 'Icon 3'],
- icons: [
- '/static/media/storybook/assets/carto-symbol.svg',
- '/static/media/storybook/assets/carto-symbol.svg',
- '/static/media/storybook/assets/carto-symbol.svg'
- ]
+ icons: [ICON, ICON, ICON]
}
}
];
- return ;
+ return ;
};
const LegendRampTemplate = () => {
- const layersDiscontinuous = [
+ const layers = [
{
id: 0,
- title: 'Ramp Layer',
+ title: 'Ramp Discontinuous',
visible: true,
legend: {
type: 'bins',
colors: ['#000', '#00F', '#0F0', '#F00'],
- labels: [100, 200, 300]
+ labels: [100, 200, 300],
+ showMinMax: true
}
- }
- ];
-
- const layersContinuous = [
+ },
{
- id: 0,
- title: 'Ramp Layer',
+ id: 1,
+ title: 'Ramp Continuous',
visible: true,
legend: {
type: 'continuous_ramp',
@@ -220,12 +274,7 @@ const LegendRampTemplate = () => {
}
];
- return (
- <>
-
-
- >
- );
+ return ;
};
const LegendProportionTemplate = () => {
@@ -236,12 +285,12 @@ const LegendProportionTemplate = () => {
visible: true,
legend: {
type: 'proportion',
- labels: [100, 500]
- // avg: 450
+ labels: [100, 500],
+ showMinMax: true
}
}
];
- return ;
+ return ;
};
const LegendCustomTemplate = () => {
@@ -251,11 +300,14 @@ const LegendCustomTemplate = () => {
title: 'Single Layer',
visible: true,
legend: {
- children: Legend custom
+ type: 'custom'
}
}
];
- return ;
+ const customLegendTypes = {
+ custom: () => Custom Legend
+ };
+ return ;
};
const LegendNoChildrenTemplate = () => {
@@ -267,16 +319,14 @@ const LegendNoChildrenTemplate = () => {
legend: {}
}
];
- return ;
+ return ;
};
-export const Playground = Template.bind({});
-
export const SingleLayer = LegendTemplate.bind({});
export const MultiLayer = LegendMultiTemplate.bind({});
export const MultiLayerCollapsed = LegendMultiTemplateCollapsed.bind({});
export const NotFound = LegendNotFoundTemplate.bind({});
-export const LegendWithOpacityControl = LegendWithOpacityTemplate.bind({});
+export const LegendWithoutOpacityControl = LegendWithoutOpacityTemplate.bind({});
export const Categories = LegendCategoriesTemplate.bind({});
export const CategoriesAsStroke = LegendCategoriesStrokeTemplate.bind({});
export const Icon = LegendIconTemplate.bind({});
diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js
index eaaafa55d..570e37fe7 100644
--- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js
+++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js
@@ -1,5 +1,6 @@
import React from 'react';
-import LegendCategories from '../../../../src/widgets/legend/LegendCategories';
+import LegendCategories from '../../../../src/widgets/legend/legend-types/LegendCategories';
+import { IntlProvider } from 'react-intl';
const DEFAULT_LEGEND = {
legend: {
@@ -26,7 +27,11 @@ const options = {
export default options;
const Template = (args) => {
- return ;
+ return (
+
+
+
+ );
};
export const Default = Template.bind({});
diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js
index a894ec4f7..6dd9b02ed 100644
--- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js
+++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js
@@ -1,5 +1,5 @@
import React from 'react';
-import LegendIcon from '../../../../src/widgets/legend/LegendIcon';
+import LegendIcon from '../../../../src/widgets/legend/legend-types/LegendIcon';
const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`;
diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js
index 65d683f35..2ba9cd02f 100644
--- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js
+++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js
@@ -1,5 +1,6 @@
import React from 'react';
-import LegendProportion from '../../../../src/widgets/legend/LegendProportion';
+import LegendProportion from '../../../../src/widgets/legend/legend-types/LegendProportion';
+import { IntlProvider } from 'react-intl';
const DEFAULT_LEGEND = {
legend: {
@@ -25,7 +26,11 @@ const options = {
export default options;
const Template = (args) => {
- return ;
+ return (
+
+
+
+ );
};
export const Default = Template.bind({});
diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js
index 234ad5777..7d7de78e9 100644
--- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js
+++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js
@@ -1,5 +1,5 @@
import React from 'react';
-import LegendRamp from '../../../../src/widgets/legend/LegendRamp';
+import LegendRamp from '../../../../src/widgets/legend/legend-types/LegendRamp';
const DEFAULT_LEGEND = {
legend: {
diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js
new file mode 100644
index 000000000..8d805969b
--- /dev/null
+++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js
@@ -0,0 +1,215 @@
+import LEGEND_TYPES from '../../../src/widgets/legend/legend-types/LegendTypes';
+
+export const fixtures = [
+ {
+ id: 'basemap',
+ title: 'Basemap',
+ collapsible: true,
+ switchable: false,
+ showOpacityControl: false,
+ legend: {
+ type: 'basemap',
+ attr: 'attr',
+ select: {
+ label: 'Select basemap',
+ value: 'light',
+ options: [
+ { label: 'Light', value: 'light' },
+ { label: 'Dark', value: 'dark' }
+ ]
+ }
+ }
+ },
+ {
+ id: 'applicants',
+ title: 'Applicants',
+ visible: false,
+ switchable: true,
+ opacity: 0.4,
+ showOpacityControl: true,
+ minZoom: 4,
+ maxZoom: 9,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.ICON,
+ icons: [
+ `data:image/svg+xml,`
+ ].map((txt) => txt.replace(/#/g, '%23')),
+ labels: ['Applicants']
+ }
+ },
+ {
+ id: 'avland',
+ title: 'Available Land',
+ visible: true,
+ switchable: true,
+ legend: [
+ {
+ attr: 'category',
+ collapsed: false,
+ type: LEGEND_TYPES.ICON,
+ icons: [
+ `data:image/svg+xml,`
+ ].map((txt) => txt.replace(/#/g, '%23')),
+ colors: ['#D40511'],
+ labels: ['Available land']
+ },
+ {
+ attr: 'population',
+ collapsed: false,
+ type: LEGEND_TYPES.PROPORTION,
+ labels: [1, 1000]
+ }
+ ]
+ },
+ {
+ id: 'catchment',
+ title: 'Catchment Area',
+ visible: true,
+ switchable: true,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.CATEGORY,
+ colors: [`#FC483F33`],
+ labels: ['Catchment Area']
+ }
+ },
+ {
+ id: 'cust-loc',
+ title: 'Customer Locations',
+ visible: false,
+ switchable: true,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.CATEGORY,
+ colors: ['#C8C8C8'],
+ labels: ['Others']
+ }
+ },
+ {
+ id: 'employees',
+ title: 'Employees',
+ visible: false,
+ switchable: true,
+ opacity: 0.4,
+ showOpacityControl: true,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.ICON,
+ icons: [
+ `data:image/svg+xml,`
+ ].map((txt) => txt.replace(/#/g, '%23')),
+ labels: ['Employees']
+ }
+ },
+ {
+ id: 'existing-ops',
+ title: 'DSC Existing Operations',
+ visible: true,
+ switchable: true,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.ICON,
+ icons: [
+ `data:image/svg+xml,`
+ ].map((txt) => txt.replace(/#/g, '%23')),
+ labels: ['DSC Existing Operations']
+ }
+ },
+ {
+ id: 'spatial-index',
+ title: 'Spatial Index',
+ visible: true,
+ opacity: 100 / 255,
+ switchable: true,
+ showOpacityControl: true,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.BINS,
+ labels: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1],
+ colors: [
+ '#ffffe5',
+ '#f7fcb9',
+ '#d9f0a3',
+ '#addd8e',
+ '#78c679',
+ '#41ab5d',
+ '#238443',
+ '#006837',
+ '#004529',
+ '#003529'
+ ]
+ }
+ },
+ {
+ id: 'intermodal-points',
+ title: 'Intermodal points',
+ visible: false,
+ switchable: true,
+ legend: {
+ collapsed: false,
+ type: LEGEND_TYPES.ICON,
+ attr: 'icon_category',
+ icons: [
+ `data:image/svg+xml,`,
+ `data:image/svg+xml,`,
+ `data:image/svg+xml,`,
+ `data:image/svg+xml,`,
+ `data:image/svg+xml,`
+ ].map((txt) => txt.replace(/#/g, '%23')),
+ labels: ['Airport', 'Dry Port', 'Port', 'Rail Hub', 'Others']
+ }
+ }
+];
diff --git a/packages/react-widgets/src/widgets/LegendWidget.js b/packages/react-widgets/src/widgets/LegendWidget.js
index 48797b9ba..b13faef27 100644
--- a/packages/react-widgets/src/widgets/LegendWidget.js
+++ b/packages/react-widgets/src/widgets/LegendWidget.js
@@ -4,32 +4,29 @@ import { LegendWidgetUI } from '@carto/react-ui';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import sortLayers from './utils/sortLayers';
+import { useMediaQuery } from '@mui/material';
/**
* Renders a component
* @param {object} props
* @param {string} [props.title] - Title of the widget.
- * @param {Object.} [props.customLayerOptions] - Allow to add custom controls to a legend item to tweak the associated layer.
* @param {Object.} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered.
* @param {boolean} [props.initialCollapsed] - Define initial collapsed value. false by default.
* @param {string[]} [props.layerOrder] - Array of layer identifiers. Defines the order of layer legends. [] by default.
-
+ * @returns {React.ReactNode}
*/
-function LegendWidget({
- customLayerOptions,
- customLegendTypes,
- initialCollapsed,
- layerOrder = [],
- title
-}) {
+function LegendWidget({ customLegendTypes, initialCollapsed, layerOrder = [], title }) {
const dispatch = useDispatch();
const layers = useSelector((state) =>
sortLayers(
Object.values(state.carto.layers).filter((layer) => !!layer.legend),
layerOrder
- )
+ ).filter((l) => !!l.legend)
);
+
const [collapsed, setCollapsed] = useState(initialCollapsed);
+ const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
+ const { zoom, maxZoom, minZoom } = useSelector((state) => state.carto.viewState);
if (!layers.length) {
return null;
@@ -57,7 +54,31 @@ function LegendWidget({
dispatch(
updateLayer({
id,
- layerAttributes: { legend: { collapsed } }
+ layerAttributes: { collapsed }
+ })
+ );
+ };
+
+ const handleSelectionChange = ({ id, index, selection }) => {
+ const layer = layers.find((layer) => layer.id === id);
+ const isMultiple = Array.isArray(layer.legend);
+ const legend = isMultiple ? layer.legend : layer.legend[index];
+ const newLegend = {
+ ...legend,
+ select: {
+ ...legend.select,
+ value: selection
+ }
+ };
+
+ dispatch(
+ updateLayer({
+ id,
+ layerAttributes: {
+ legend: isMultiple
+ ? layer.legend.map((l, i) => (i === index ? newLegend : l))
+ : newLegend
+ }
})
);
};
@@ -66,13 +87,17 @@ function LegendWidget({
);
}
diff --git a/tsconfig.json b/tsconfig.json
index 7c51700ce..fb6c3a177 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,7 @@
"strict": true,
"noImplicitAny": false,
"allowJs": true,
- "checkJs": true,
+ "checkJs": false,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,