Skip to content

Commit

Permalink
Legend for waffle maps
Browse files Browse the repository at this point in the history
  • Loading branch information
mthh committed Oct 30, 2024
1 parent 0658887 commit 12c1459
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 9 deletions.
212 changes: 212 additions & 0 deletions src/components/LegendRenderer/WaffleLegendRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
createEffect,
createMemo,
For,
type JSX,
onMount,
Show,
} from 'solid-js';

// Helpers
import { useI18nContext } from '../../i18n/i18n-solid';
import { findLayerById } from '../../helpers/layers';

// Sub-components and helpers for legend rendering
import {
bindElementsLegend,
computeRectangleBox,
getTextSize,
makeLegendSettingsModal,
makeLegendText,
RectangleBox,
triggerContextMenuLegend,
} from './common.tsx';

// Stores
import { layersDescriptionStore } from '../../store/LayersDescriptionStore';
import { applicationSettingsStore } from '../../store/ApplicationSettingsStore';

// Types / Interfaces / Enums
import { type LayerDescriptionWaffle, type WaffleLegend } from '../../global.d';

const defaultSpacing = applicationSettingsStore.defaultLegendSettings.spacing;

function verticalLegendWaffle(
legend: WaffleLegend,
): JSX.Element {
const { LL } = useI18nContext();

const layer = findLayerById(
layersDescriptionStore.layers,
legend.layerId,
)! as LayerDescriptionWaffle;

const heightTitle = createMemo(
() => getTextSize(
legend.title.text,
legend.title.fontSize,
legend.title.fontFamily,
).height + defaultSpacing,
);

const distanceToTop = createMemo(() => {
let vDistanceToTop = 0;
if (legend.title) {
vDistanceToTop += heightTitle() + defaultSpacing;
}
if (legend.subtitle.text) {
vDistanceToTop += getTextSize(
legend.subtitle.text,
legend.subtitle.fontSize,
legend.subtitle.fontFamily,
).height + defaultSpacing;
}
// vDistanceToTop += legend.boxSpacing / 2;
return vDistanceToTop;
});

const boxHeightAndSpacing = createMemo(
() => legend.boxHeight + legend.boxSpacing,
);

const labelsAndColors = createMemo(
() => layer.rendererParameters.variables
.map(({ displayName, color }) => [displayName, color])
.toReversed(),
);

const positionSymbolValue = createMemo(
() => distanceToTop()
+ labelsAndColors().length * boxHeightAndSpacing() - legend.boxSpacing
+ (layer.rendererParameters.symbolType === 'circle' ? layer.rendererParameters.size / 2 : 0)
+ legend.spacingBelowBoxes,
);

const positionNote = createMemo(
() => positionSymbolValue()
+ (layer.rendererParameters.symbolType === 'circle' ? layer.rendererParameters.size / 2 : layer.rendererParameters.size)
+ defaultSpacing * 3,
);

let refElement: SVGGElement;

onMount(() => {
// We need to wait for the legend to be rendered before we can compute its size
// and bind the drag behavior and the mouse enter / leave behavior.
bindElementsLegend(refElement, legend);
});

createEffect(() => {
if (refElement && layer.visible && legend.visible) {
computeRectangleBox(
refElement,
distanceToTop(),
boxHeightAndSpacing(),
heightTitle(),
positionNote(),
legend.boxHeight,
legend.boxWidth,
legend.spacingBelowBoxes,
legend.title.text,
legend.subtitle.text,
legend.note.text,
layer.rendererParameters.variables,
);
}
});

return <g
ref={refElement!}
id={legend.id}
class="legend waffle"
for={layer.id}
transform={`translate(${legend.position[0]}, ${legend.position[1]})`}
visibility={layer.visible && legend.visible ? undefined : 'hidden'}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
triggerContextMenuLegend(e, legend.id, LL);
}}
onDblClick={() => { makeLegendSettingsModal(legend.id, LL); }}
style={{ cursor: 'grab' }}
>
<RectangleBox backgroundRect={legend.backgroundRect} />
{ makeLegendText(legend.title, [0, 0], 'title') }
{ makeLegendText(legend.subtitle, [0, heightTitle()], 'subtitle') }
<g class="legend-content">
<For each={labelsAndColors()}>
{
([_, color], i) => <rect
fill={color}
x={0}
y={distanceToTop() + i() * boxHeightAndSpacing()}
rx={legend.boxCornerRadius}
ry={legend.boxCornerRadius}
width={legend.boxWidth}
height={legend.boxHeight}
stroke={legend.stroke ? layer.strokeColor : undefined}
/>
}
</For>
<For each={labelsAndColors()}>
{
([categoryName, _], i) => <text
x={legend.boxWidth + defaultSpacing}
y={distanceToTop() + i() * boxHeightAndSpacing() + (legend.boxHeight / 2)}
font-size={legend.labels.fontSize}
font-family={legend.labels.fontFamily}
font-style={legend.labels.fontStyle}
font-weight={legend.labels.fontWeight}
fill={legend.labels.fontColor}
text-anchor="start"
dominant-baseline="middle"
>{ categoryName }</text>
}
</For>
<Show when={layer.rendererParameters.symbolType === 'circle'}>
<circle
cx={legend.boxWidth / 2}
cy={positionSymbolValue()}
r={layer.rendererParameters.size / 2}
fill={'lightgray'}
stroke={layer.strokeColor}
/>
</Show>
<Show when={layer.rendererParameters.symbolType === 'square'}>
<rect
x={legend.boxWidth / 2 - layer.rendererParameters.size / 2}
y={positionSymbolValue()}
width={layer.rendererParameters.size}
height={layer.rendererParameters.size}
fill={'lightgray'}
stroke={layer.strokeColor}
/>
</Show>
<text
x={legend.boxWidth + defaultSpacing}
y={positionSymbolValue() + (layer.rendererParameters.symbolType === 'circle' ? 0 : layer.rendererParameters.size / 2)}
font-size={legend.valueText.fontSize}
font-family={legend.valueText.fontFamily}
font-style={legend.valueText.fontStyle}
font-weight={legend.valueText.fontWeight}
fill={legend.valueText.fontColor}
text-anchor="start"
dominant-baseline="middle"
>{legend.valueText.text}</text>
</g>
{
makeLegendText(
legend.note,
[0, positionNote()],
'note',
)
}
</g>;
}

export default function legendWaffle(
legend: WaffleLegend,
): JSX.Element {
// We only implement the vertical legend for now
return verticalLegendWaffle(legend);
}
5 changes: 5 additions & 0 deletions src/components/MapZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import legendCategoricalChoroplethBarchart from './LegendRenderer/CategoricalCho
import legendChoroplethHistogram from './LegendRenderer/ChoroplethHistogramLegend.tsx';
import lmScatterPlot from './LegendRenderer/LMScatterPlotRenderer.tsx';
import legendCategoricalPictogram from './LegendRenderer/CategoricalPictogramLegendRenderer.tsx';
import legendWaffle from './LegendRenderer/WaffleLegendRenderer.tsx';

// Types and enums
import {
Expand Down Expand Up @@ -124,6 +125,7 @@ import {
type ChoroplethHistogramLegend,
type LayerDescriptionCategoricalPictogram,
type LayerDescriptionWaffle,
type WaffleLegend,
} from '../global.d';

// Styles
Expand Down Expand Up @@ -200,6 +202,9 @@ const dispatchLegendRenderer = (legend: Legend) => {
if (legend.type === 'categoricalPictogram') {
return legendCategoricalPictogram(legend as CategoricalPictogramLegend);
}
if (legend.type === 'waffle') {
return legendWaffle(legend as WaffleLegend);
}
return null;
};

Expand Down
96 changes: 92 additions & 4 deletions src/components/Modals/LegendSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
unproxify,
} from '../../helpers/common';
import { findLayerById } from '../../helpers/layers';
import { Mround, round } from '../../helpers/math';
import { round } from '../../helpers/math';
import type { TranslationFunctions } from '../../i18n/i18n-types';

// Subcomponents
Expand All @@ -47,10 +47,12 @@ import {
type CategoricalChoroplethLegend,
type CategoricalPictogramLegend,
type ChoroplethHistogramLegend,
type ChoroplethLegend, type ClassificationParameters,
type ChoroplethLegend,
type ClassificationParameters,
DefaultLegend,
type DiscontinuityLegend,
type LabelsLegend, LayerDescription,
type LabelsLegend,
LayerDescription,
type LayerDescriptionCategoricalChoropleth,
type LayerDescriptionChoropleth,
type LayerDescriptionGriddedLayer,
Expand All @@ -65,7 +67,7 @@ import {
type ProportionalSymbolsLegend,
type ProportionalSymbolsParameters,
type ProportionalSymbolsRatioParameters,
RepresentationType,
RepresentationType, WaffleLegend,
} from '../../global.d';

/**
Expand Down Expand Up @@ -1308,6 +1310,86 @@ function makeSettingsScatterPlot(
</>;
}

function makeSettingsWaffle(
legend: WaffleLegend,
LL: Accessor<TranslationFunctions>,
): JSX.Element {
const [
displayMoreOptions,
setDisplayMoreOptions,
] = createSignal<boolean>(false);

return <>
<FieldText legend={legend} LL={LL} role={'title'}/>
<FieldText legend={legend} LL={LL} role={'subtitle'}/>
<FieldText legend={legend} LL={LL} role={'note'}/>
<InputFieldNumber
label={ LL().Legend.Modal.BoxWidth() }
value={ legend.boxWidth }
min={0}
max={100}
step={1}
onChange={(v) => debouncedUpdateProps(legend.id, ['boxWidth'], v)}
/>
<InputFieldNumber
label={ LL().Legend.Modal.BoxHeight() }
value={ legend.boxHeight }
min={0}
max={100}
step={1}
onChange={(v) => debouncedUpdateProps(legend.id, ['boxHeight'], v)}
/>
<InputFieldNumber
label={ LL().Legend.Modal.BoxCornerRadius() }
value={ legend.boxCornerRadius }
min={0}
max={100}
step={1}
strictMin={true}
onChange={(v) => {
debouncedUpdateProps(legend.id, ['boxCornerRadius'], v);
}}
/>
<InputFieldNumber
label={ LL().Legend.Modal.BoxSpacing() }
value={ legend.boxSpacing }
min={0}
max={100}
step={1}
onChange={(v) => {
debouncedUpdateProps(legend.id, ['boxSpacing'], v);
}}
/>
<InputFieldNumber
label={'Spacing below color boxes'}
value={legend.spacingBelowBoxes}
onChange={(v) => {
debouncedUpdateProps(legend.id, ['spacingBelowBoxes'], v);
}}
min={0}
max={100}
step={1}
/>
<OptionBackgroundRectangle legend={legend} LL={LL} />
<div
onClick={() => setDisplayMoreOptions(!displayMoreOptions())}
style={{ cursor: 'pointer' }}
>
<p class="label">
{ LL().Legend.Modal.FontProperties() }
<FaSolidPlus style={{ 'vertical-align': 'text-bottom', margin: 'auto 0.5em' }} />
</p>
</div>
<Show when={displayMoreOptions()}>
<TextOptionTable
legend={legend}
LL={LL}
textProperties={['title', 'subtitle', 'labels', 'note']}
/>
</Show>
</>;
}

function makeSettingsCategoricalPictogram(
legend: CategoricalPictogramLegend,
LL: Accessor<TranslationFunctions>,
Expand Down Expand Up @@ -1401,6 +1483,12 @@ function getInnerPanel(legend: Legend, LL: Accessor<TranslationFunctions>): JSX.
LL,
);
}
if (legend.type === LegendType.waffle) {
return makeSettingsWaffle(
legend as WaffleLegend,
LL,
);
}
return <></>;
}

Expand Down
Loading

0 comments on commit 12c1459

Please sign in to comment.