Skip to content

Commit

Permalink
Various stuff for creating and drawing waffles
Browse files Browse the repository at this point in the history
  • Loading branch information
mthh committed Oct 29, 2024
1 parent 03d3498 commit 36d4303
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 20 deletions.
13 changes: 7 additions & 6 deletions src/components/MapRenderer/WaffleMapRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ export default function waffleRenderer(
}));

// Central position of the symbol block
const offsetCentroidX = (
(params.size + params.spacing) * (params.columns) - (params.size * 1.5));
const offsetCentroidX = params.symbolType === 'circle'
? (params.size + params.spacing) * (params.columns / 2) - (params.size * 0.5)
: (params.size + params.spacing) * (params.columns / 2);

let symbolIndex = 0; // Counter for symbol position

Expand All @@ -83,17 +84,17 @@ export default function waffleRenderer(
{(variable) => <For each={Array.from({ length: variable.count })}>
{(_, i) => {
const tx = Mround(
(symbolIndex % params.columns) * (2 * params.size + params.spacing),
(symbolIndex % params.columns) * (params.size + params.spacing),
);
const ty = Mfloor(
Mfloor(symbolIndex / params.columns) * (2 * params.size + params.spacing),
Mfloor(symbolIndex / params.columns) * (params.size + params.spacing),
);
symbolIndex += 1; // Increment for next position
return params.symbolType === 'circle' ? (
<circle
cx={projectedCoords()[0] - offsetCentroidX + tx}
cy={projectedCoords()[1] - params.size - ty}
r={params.size}
cy={projectedCoords()[1] - (params.size / 2) - ty}
r={params.size / 2}
fill={variable.color}
/>
) : (
Expand Down
154 changes: 148 additions & 6 deletions src/components/PortrayalOption/WaffleSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Imports from solid-js
import {
createEffect,
createSignal,
For,
type JSX,
type JSX, on,
Show,
} from 'solid-js';
import { produce } from 'solid-js/store';

Expand All @@ -13,15 +15,17 @@ import { yieldOrContinue } from 'main-thread-scheduling';
import { useI18nContext } from '../../i18n/i18n-solid';
import { randomColorFromCategoricalPalette } from '../../helpers/color';
import {
findSuitableName,
findSuitableName, isFiniteNumber,
} from '../../helpers/common';
import { coordsPointOnFeature } from '../../helpers/geo';
import { generateIdLayer } from '../../helpers/layers';
import { getPossibleLegendPosition } from '../LegendRenderer/common.tsx';
import { generateIdLegend } from '../../helpers/legends';
import { Mmax, Mmin } from '../../helpers/math';

// Sub-components
import ButtonValidation from '../Inputs/InputButtonValidation.tsx';
import InputFieldMultiSelect from '../Inputs/InputMultiSelect.tsx';
import InputFieldNumber from '../Inputs/InputNumber.tsx';
import InputFieldSelect from '../Inputs/InputSelect.tsx';
import InputResultName from './InputResultName.tsx';
Expand All @@ -47,7 +51,83 @@ import {
WaffleParameters,
} from '../../global.d';
import type { PortrayalSettingsProps } from './common';
import InputFieldMultiSelect from '../Inputs/InputMultiSelect.tsx';
import MessageBlock from '../MessageBlock.tsx';

function guessSymbolValue(
layerId: string,
selectedVariables: { name: string, displayName: string, color: string }[],
) {
if (selectedVariables.length < 2) {
return 0;
}

const ld = layersDescriptionStore.layers.find((l) => l.id === layerId)!;

let minSum = Infinity;
let maxSum = -Infinity;

// We want to find the maximum sum and the minimum sum
// (between all the features) of the selected variables
ld.data.features.forEach((feature) => {
let sum = 0;
selectedVariables.forEach((variable) => {
if (isFiniteNumber(feature.properties[variable.name])) {
sum += +feature.properties[variable.name];
}
});
maxSum = Mmax(maxSum, sum);
minSum = Mmin(minSum, sum);
});

// We want to find a divisor (ideally a multiple of 10) that
// allows the user to encode the sum of the selected variables
// in a stack of symbols (without having too much symbols for the
// features that have the largest sum and without having no symbol
// for the features that have the smallest sum)
let divisor = 0;
const step = maxSum > 100 ? 10 : 1;
while (maxSum / divisor > step) {
if (minSum / divisor < selectedVariables.length) {
break;
}
divisor += step;
}

return divisor;
}

function isValidSymbolValue(
layerId: string,
selectedVariables: { name: string, displayName: string, color: string }[],
symbolValue: number,
) {
const ld = layersDescriptionStore.layers.find((l) => l.id === layerId)!;

let minNumberOfSymbols = Infinity;
let maxNumberOfSymbols = -Infinity;

// We want to find the number of symbols that will be displayed
// for each feature
ld.data.features.forEach((feature) => {
let numberOfSymbols = 0;
selectedVariables.forEach((variable) => {
if (isFiniteNumber(feature.properties[variable.name])) {
numberOfSymbols += +feature.properties[variable.name] / symbolValue;
}
});

maxNumberOfSymbols = Mmax(maxNumberOfSymbols, numberOfSymbols);
minNumberOfSymbols = Mmin(minNumberOfSymbols, numberOfSymbols);
});

if (maxNumberOfSymbols > 1000) {
return { valid: false, reason: 'tooManySymbols' };
}
// if (minNumberOfSymbols < 1) {
// return { valid: false, reason: 'tooFewSymbols' };
// }
return { valid: true };
}

function onClickValidate(
referenceLayerId: string,
Expand Down Expand Up @@ -203,13 +283,13 @@ export default function WaffleSettings(
const [
symbolSize,
setSymbolSize,
] = createSignal<number>(5);
] = createSignal<number>(10);

// Number of columns
const [
columns,
setColumns,
] = createSignal<number>(10);
] = createSignal<number>(5);

// Space between symbols
const [
Expand All @@ -223,6 +303,20 @@ export default function WaffleSettings(
setSymbolValue,
] = createSignal<number>(0);

createEffect(
on(
() => selectedVariables(),
() => {
setSymbolValue(
guessSymbolValue(
layerDescription.id,
selectedVariables(),
),
);
},
),
);

const makePortrayal = async () => {
// Compute a suitable name for the new layer
const layerName = findSuitableName(
Expand Down Expand Up @@ -334,12 +428,60 @@ export default function WaffleSettings(
min={0}
max={Infinity}
step={1}
disabled={selectedVariables().length < 2}
bindKeyUpAsChange={true}
/>
<Show when={
selectedVariables().length >= 2
&& isValidSymbolValue(
layerDescription.id,
selectedVariables(),
symbolValue(),
).reason === 'tooManySymbols'
}>
<MessageBlock type={'danger'} useIcon={true}>
{
LL().FunctionalitiesSection
.WaffleOptions.WarningTooManySymbols({
value: guessSymbolValue(layerDescription.id, selectedVariables()),
})
}
</MessageBlock>
</Show>
<Show when={
selectedVariables().length >= 2
&& isValidSymbolValue(
layerDescription.id,
selectedVariables(),
symbolValue(),
).reason === 'tooFewSymbols'
}>
<MessageBlock type={'danger'} useIcon={true}>
{
LL().FunctionalitiesSection
.WaffleOptions.WarningTooFewSymbols({
value: guessSymbolValue(layerDescription.id, selectedVariables()),
})
}
</MessageBlock>
</Show>
<InputResultName
value={newLayerName()}
onKeyUp={ (value) => { setNewLayerName(value); }}
onEnter={makePortrayal}
/>
<ButtonValidation label={ LL().FunctionalitiesSection.CreateLayer() } onClick={makePortrayal} />
<ButtonValidation
label={ LL().FunctionalitiesSection.CreateLayer() }
onClick={makePortrayal}
disabled={
!(selectedVariables().length >= 2
&& isValidSymbolValue(
layerDescription.id,
selectedVariables(),
symbolValue(),
).valid
)
}
/>
</div>;
}
16 changes: 8 additions & 8 deletions src/helpers/svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,29 +352,29 @@ export const redrawPaths = (svgElement: SVGSVGElement & IZoomable) => {
(gg as SVGGElement & ID3Element).__data__.geometry,
);
if (!Number.isNaN(coords[0])) {
const offsetCentroidX = (
(size + spacing) * (columns) - (size * 1.5)
);
const offsetCentroidX = symbolType === 'circle'
? (size + spacing) * (columns / 2) - (size * 0.5)
: (size + spacing) * (columns / 2);
let symbolIndex = 0;
if (symbolType === 'circle') {
gg.querySelectorAll('circle').forEach((c) => {
const tx = Mround(
(symbolIndex % columns) * (2 * size + spacing),
(symbolIndex % columns) * (size + spacing),
);
const ty = Mfloor(
Mfloor(symbolIndex / columns) * (2 * size + spacing),
Mfloor(symbolIndex / columns) * (size + spacing),
);
symbolIndex += 1;
c.setAttribute('cx', `${coords[0] - offsetCentroidX + tx}`);
c.setAttribute('cy', `${coords[1] - size - ty}`);
c.setAttribute('cy', `${coords[1] - (size / 2) - ty}`);
});
} else { // symbolType === 'square'
gg.querySelectorAll('rect').forEach((r) => {
const tx = Mround(
(symbolIndex % columns) * (2 * size + spacing),
(symbolIndex % columns) * (size + spacing),
);
const ty = Mfloor(
Mfloor(symbolIndex / columns) * (2 * size + spacing),
Mfloor(symbolIndex / columns) * (size + spacing),
);
r.setAttribute('x', `${coords[0] - offsetCentroidX + tx}`);
r.setAttribute('y', `${coords[1] - size - ty}`);
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,8 @@ const en = {
SymbolSize: 'Symbol size',
SymbolValue: 'Value expressed by each symbol',
NewLayerName: 'Waffle_{layerName}',
WarningTooManySymbols: 'The value represented by each symbol is too small to be displayed correctly (too many symbols would be generated by choosing this value). Please choose a larger value (e.g. {value}).',
WarningTooFewSymbols: 'The value represented by each symbol is too high to correctly distinguish the different entities (too few symbols would be generated by choosing this value). Please choose a smaller value (e.g. {value}).',
},
},
FormulaInput: {
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/fr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,8 @@ const fr = {
SymbolSize: 'Taille du symbole',
SymbolValue: 'Valeur représentée par chaque symbole',
NewLayerName: 'Gaufre_{layerName}',
WarningTooManySymbols: 'La valeur représentée par chaque symbole est trop faible pour être affichée correctement (trop de symboles seraient générés par le choix de cette valeur). Veuillez choisir une valeur plus grande (par exemple {value}).',
WarningTooFewSymbols: 'La valeur représentée par chaque symbole est trop élevée pour permettre de distinguer correctement les différentes entités (trop peu de symboles seraient générés par le choix de cette valeur). Veuillez choisir une valeur plus petite (par exemple {value}).',
},
},
FormulaInput: {
Expand Down
18 changes: 18 additions & 0 deletions src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4436,6 +4436,16 @@ type RootTranslation = {
* @param {unknown} layerName
*/
NewLayerName: RequiredParams<'layerName'>
/**
* T​h​e​ ​v​a​l​u​e​ ​r​e​p​r​e​s​e​n​t​e​d​ ​b​y​ ​e​a​c​h​ ​s​y​m​b​o​l​ ​i​s​ ​t​o​o​ ​s​m​a​l​l​ ​t​o​ ​b​e​ ​d​i​s​p​l​a​y​e​d​ ​c​o​r​r​e​c​t​l​y​ ​(​t​o​o​ ​m​a​n​y​ ​s​y​m​b​o​l​s​ ​w​o​u​l​d​ ​b​e​ ​g​e​n​e​r​a​t​e​d​ ​b​y​ ​c​h​o​o​s​i​n​g​ ​t​h​i​s​ ​v​a​l​u​e​)​.​ ​P​l​e​a​s​e​ ​c​h​o​o​s​e​ ​a​ ​l​a​r​g​e​r​ ​v​a​l​u​e​ ​(​e​.​g​.​ ​{​v​a​l​u​e​}​)​.
* @param {unknown} value
*/
WarningTooManySymbols: RequiredParams<'value'>
/**
* T​h​e​ ​v​a​l​u​e​ ​r​e​p​r​e​s​e​n​t​e​d​ ​b​y​ ​e​a​c​h​ ​s​y​m​b​o​l​ ​i​s​ ​t​o​o​ ​h​i​g​h​ ​t​o​ ​c​o​r​r​e​c​t​l​y​ ​d​i​s​t​i​n​g​u​i​s​h​ ​t​h​e​ ​d​i​f​f​e​r​e​n​t​ ​e​n​t​i​t​i​e​s​ ​(​t​o​o​ ​f​e​w​ ​s​y​m​b​o​l​s​ ​w​o​u​l​d​ ​b​e​ ​g​e​n​e​r​a​t​e​d​ ​b​y​ ​c​h​o​o​s​i​n​g​ ​t​h​i​s​ ​v​a​l​u​e​)​.​ ​P​l​e​a​s​e​ ​c​h​o​o​s​e​ ​a​ ​s​m​a​l​l​e​r​ ​v​a​l​u​e​ ​(​e​.​g​.​ ​{​v​a​l​u​e​}​)​.
* @param {unknown} value
*/
WarningTooFewSymbols: RequiredParams<'value'>
}
}
FormulaInput: {
Expand Down Expand Up @@ -10280,6 +10290,14 @@ export type TranslationFunctions = {
* Waffle_{layerName}
*/
NewLayerName: (arg: { layerName: unknown }) => LocalizedString
/**
* The value represented by each symbol is too small to be displayed correctly (too many symbols would be generated by choosing this value). Please choose a larger value (e.g. {value}).
*/
WarningTooManySymbols: (arg: { value: unknown }) => LocalizedString
/**
* The value represented by each symbol is too high to correctly distinguish the different entities (too few symbols would be generated by choosing this value). Please choose a smaller value (e.g. {value}).
*/
WarningTooFewSymbols: (arg: { value: unknown }) => LocalizedString
}
}
FormulaInput: {
Expand Down

0 comments on commit 36d4303

Please sign in to comment.