From 26ef5948f84e42ce88d8b9dccaf540152cd5b27e Mon Sep 17 00:00:00 2001
From: AlasDiablo <25723276+AlasDiablo@users.noreply.github.com>
Date: Thu, 4 Jul 2024 10:05:26 +0200
Subject: [PATCH] feat(treemap): make treemap compatible with flat data
---
src/app/custom/translations.tsv | 2 +
.../field-set/FormatFieldSetPreview.js | 14 +++-
.../vega/component/tree-map/TreeMapAdmin.js | 69 +++++++++++++++--
.../vega/component/tree-map/TreeMapData.js | 77 ++++++++++++++++++-
.../vega/component/tree-map/TreeMapView.js | 28 ++++++-
src/app/js/formats/vega/models/TreeMap.js | 68 +++++++++++++---
6 files changed, 235 insertions(+), 23 deletions(-)
diff --git a/src/app/custom/translations.tsv b/src/app/custom/translations.tsv
index 4c3f8bfee..69110c3ad 100644
--- a/src/app/custom/translations.tsv
+++ b/src/app/custom/translations.tsv
@@ -1098,3 +1098,5 @@
"ejs_variable_list" "Data from a routine is displayed using an HTML template based on EJS syntax. You can use these variables to access data and utils:" "L’affichage des données en provenance d’une routine est réalisé à l’aide d’un template HTML utilisant la syntaxe EJS. Vous pouvez utiliser ces variables pour accéder aux données et aux utilitaires :"
"ejs_data" "Variable containing the routine data" "Variable contenant les données de la routine"
"ejs_lodash" "Variable containing the Lodash function" "Variable contenant les fonctions de Lodash"
+"treemap_hierarchy_data" "Hierarchy data" "Donnée hiérarchique"
+"treemap_flat_data_type" "Data struture" "Struture des données"
diff --git a/src/app/js/formats/utils/components/field-set/FormatFieldSetPreview.js b/src/app/js/formats/utils/components/field-set/FormatFieldSetPreview.js
index a5b3c9978..2f792a977 100644
--- a/src/app/js/formats/utils/components/field-set/FormatFieldSetPreview.js
+++ b/src/app/js/formats/utils/components/field-set/FormatFieldSetPreview.js
@@ -21,16 +21,26 @@ const FormatFieldSetPreview = ({
}) => {
const ReactJson = require('react-json-view').default;
- const [datasetName, setDatasetName] = useState(datasets[0].name);
+ const [datasetName, setDatasetName] = useState('');
const [dataset, setDataset] = useState({});
+ useEffect(() => {
+ if (datasets && datasets.length >= 1) {
+ setDatasetName(datasets[0].name);
+ }
+ }, [datasets]);
+
useEffect(() => {
const newSet = datasets.find((value) => value.name === datasetName);
+ if (!newSet) {
+ setDataset({});
+ return;
+ }
setDataset({
total: newSet.total,
values: newSet.values,
});
- }, [datasetName]);
+ }, [datasets, datasetName]);
const handleDataSetChange = (event) => {
setDatasetName(event.target.value);
diff --git a/src/app/js/formats/vega/component/tree-map/TreeMapAdmin.js b/src/app/js/formats/vega/component/tree-map/TreeMapAdmin.js
index 855feba1a..47b8eef93 100644
--- a/src/app/js/formats/vega/component/tree-map/TreeMapAdmin.js
+++ b/src/app/js/formats/vega/component/tree-map/TreeMapAdmin.js
@@ -12,7 +12,6 @@ import {
FormatDataParamsFieldSet,
} from '../../../utils/components/field-set/FormatFieldSets';
import {
- Box,
FormControlLabel,
FormGroup,
MenuItem,
@@ -24,7 +23,11 @@ import {
import VegaAdvancedMode from '../../../utils/components/admin/VegaAdvancedMode';
import ColorPickerParamsAdmin from '../../../utils/components/admin/ColorPickerParamsAdmin';
import AspectRatioSelector from '../../../utils/components/admin/AspectRatioSelector';
-import { TreeMapSourceTargetWeight } from '../../../utils/dataSet';
+import {
+ StandardIdValue,
+ StandardSourceTargetWeight,
+ TreeMapSourceTargetWeight,
+} from '../../../utils/dataSet';
import VegaFieldPreview from '../../../utils/components/field-set/FormatFieldSetPreview';
import VegaToolTips from '../../../utils/components/admin/VegaToolTips';
import { TreeMapAdminView } from './TreeMapView';
@@ -35,6 +38,8 @@ export const defaultArgs = {
maxSize: 5,
orderBy: 'value/asc',
},
+ hierarchy: true,
+ flatType: 'id/value',
advancedMode: false,
advancedModeSpec: null,
tooltip: false,
@@ -58,6 +63,8 @@ const TreeMapAdmin = (props) => {
} = props;
const {
+ hierarchy,
+ flatType,
advancedMode,
advancedModeSpec,
tooltip,
@@ -70,6 +77,16 @@ const TreeMapAdmin = (props) => {
aspectRatio,
} = args;
+ const dataset = useMemo(() => {
+ if (hierarchy) {
+ return TreeMapSourceTargetWeight;
+ }
+ if (flatType === 'id/value') {
+ return StandardIdValue;
+ }
+ return StandardSourceTargetWeight;
+ }, [hierarchy, flatType]);
+
const colors = useMemo(() => {
return args.colors || defaultArgs.colors;
}, [args.colors]);
@@ -85,8 +102,12 @@ const TreeMapAdmin = (props) => {
const specBuilder = new TreeMap();
+ specBuilder.setHierarchy(hierarchy);
specBuilder.setColors(colors.split(' '));
specBuilder.setTooltip(tooltip);
+ specBuilder.setThirdTooltip(
+ hierarchy || (!hierarchy && flatType !== 'id/value'),
+ );
specBuilder.setTooltipSource(tooltipSource);
specBuilder.setTooltipTarget(tooltipTarget);
specBuilder.setTooltipWeight(tooltipWeight);
@@ -111,7 +132,15 @@ const TreeMapAdmin = (props) => {
};
const toggleAdvancedMode = () => {
- updateAdminArgs('advancedMode', !args.advancedMode, props);
+ updateAdminArgs('advancedMode', !advancedMode, props);
+ };
+
+ const toggleHierarchy = () => {
+ updateAdminArgs('hierarchy', !hierarchy, props);
+ };
+
+ const handleFlatType = (e) => {
+ updateAdminArgs('flatType', e.target.value, props);
};
const clearAdvancedModeSpec = () => {
@@ -183,6 +212,31 @@ const TreeMapAdmin = (props) => {
label={polyglot.t('advancedMode')}
/>
+
+
+ }
+ label={polyglot.t('treemap_hierarchy_data')}
+ />
+
+ {!hierarchy ? (
+
+
+
+
+ ) : null}
{advancedMode ? (
{
onValueTitleChange={handleTooltipTarget}
valueTitle={tooltipTarget}
polyglot={polyglot}
- thirdValue={true}
+ thirdValue={
+ hierarchy ||
+ (!hierarchy && flatType !== 'id/value')
+ }
onThirdValueChange={handleTooltipWeight}
thirdValueTitle={tooltipWeight}
/>
@@ -261,7 +318,7 @@ const TreeMapAdmin = (props) => {
@@ -276,6 +333,8 @@ TreeMapAdmin.propTypes = {
minValue: PropTypes.number,
orderBy: PropTypes.string,
}),
+ hierarchy: PropTypes.bool,
+ flatType: PropTypes.oneOf(['id/value', 'source/target/weight']),
advancedMode: PropTypes.bool,
advancedModeSpec: PropTypes.string,
tooltip: PropTypes.bool,
diff --git a/src/app/js/formats/vega/component/tree-map/TreeMapData.js b/src/app/js/formats/vega/component/tree-map/TreeMapData.js
index d264725c9..b2cba2cde 100644
--- a/src/app/js/formats/vega/component/tree-map/TreeMapData.js
+++ b/src/app/js/formats/vega/component/tree-map/TreeMapData.js
@@ -30,13 +30,37 @@ export default class TreeMapData {
*/
filteredNodes;
- constructor(data) {
+ /**
+ * Those variables are use when we have flat data
+ * @type {boolean}
+ */
+ hierarchy;
+ /**
+ * @type {Map}
+ */
+ initialHierarchy;
+
+ /**
+ * @param {Array<{source: string, target: string, weight: string | number, hierarchy?: string}>} data
+ * @param hierarchy
+ */
+ constructor(data, hierarchy = true) {
this.data = data;
this.idIncrement = 0;
this.ids = new Map();
this.rawNodesAndLeaves = new Map();
this.formattedNodesAndLeaves = new Map();
this.filteredNodes = new Set();
+ this.hierarchy = hierarchy;
+
+ if (!this.hierarchy) {
+ this.initialHierarchy = new Map();
+ data.forEach((datum) => {
+ if (datum.hierarchy) {
+ this.initialHierarchy.set(datum.target, datum.hierarchy);
+ }
+ });
+ }
}
/**
@@ -141,7 +165,11 @@ export default class TreeMapData {
continue;
}
- datum.hierarchy = this.createHierarchy(datum.parent);
+ if (this.hierarchy) {
+ datum.hierarchy = this.createHierarchy(datum.parent);
+ } else {
+ datum.hierarchy = this.initialHierarchy.get(datum.name);
+ }
transformedAndCleanupData.push(datum);
}
@@ -159,3 +187,48 @@ export default class TreeMapData {
return this.buildReturnable();
}
}
+
+/**
+ * @param {Array<{_id: string, value: string | number}>} values
+ * @return {Array<{source: string, target: string, weight: string | number}>}
+ */
+TreeMapData.transformIdValue = (values) => {
+ const finalValues = [];
+ values.forEach((datum) => {
+ const middleNode = `root_${datum._id}`;
+ finalValues.push({
+ source: middleNode,
+ target: datum._id,
+ weight: datum.value,
+ });
+ finalValues.push({
+ source: 'root',
+ target: middleNode,
+ weight: datum.value,
+ });
+ });
+ return finalValues;
+};
+
+/**
+ * @param {Array<{source: string, target: string, weight: string | number}>} values
+ * @return {Array<{source: string, target: string, weight: string | number, hierarchy: string}>}
+ */
+TreeMapData.transformSourceTargetWeight = (values) => {
+ const finalValues = [];
+ values.forEach((datum) => {
+ const leaves = `leaves_${datum.source}_${datum.target}`;
+ finalValues.push({
+ source: datum.source,
+ target: leaves,
+ weight: datum.weight,
+ hierarchy: { ...datum },
+ });
+ finalValues.push({
+ source: 'root',
+ target: datum.source,
+ weight: datum.weight,
+ });
+ });
+ return finalValues;
+};
diff --git a/src/app/js/formats/vega/component/tree-map/TreeMapView.js b/src/app/js/formats/vega/component/tree-map/TreeMapView.js
index a599ca99a..399e765a2 100644
--- a/src/app/js/formats/vega/component/tree-map/TreeMapView.js
+++ b/src/app/js/formats/vega/component/tree-map/TreeMapView.js
@@ -26,6 +26,8 @@ const TreeMapView = (props) => {
const {
data,
field,
+ hierarchy,
+ flatType,
advancedMode,
advancedModeSpec,
tooltip,
@@ -43,12 +45,22 @@ const TreeMapView = (props) => {
return data;
}
- const treeMapDataBuilder = new TreeMapData(data.values);
+ let values = data.values;
+ if (!hierarchy) {
+ if (flatType === 'id/value') {
+ values = TreeMapData.transformIdValue(data.values);
+ }
+ if (flatType === 'source/target/weight') {
+ values = TreeMapData.transformSourceTargetWeight(data.values);
+ }
+ }
+
+ const treeMapDataBuilder = new TreeMapData(values, hierarchy);
return {
...data,
values: treeMapDataBuilder.build(),
};
- }, [data]);
+ }, [data, hierarchy, flatType]);
const { ref, width } = useSizeObserver();
const [error, setError] = useState('');
@@ -69,8 +81,12 @@ const TreeMapView = (props) => {
const specBuilder = new TreeMap();
+ specBuilder.setHierarchy(hierarchy);
specBuilder.setColors(colors.split(' '));
specBuilder.setTooltip(tooltip);
+ specBuilder.setThirdTooltip(
+ hierarchy || (!hierarchy && flatType !== 'id/value'),
+ );
specBuilder.setTooltipSource(tooltipSource);
specBuilder.setTooltipTarget(tooltipTarget);
specBuilder.setTooltipWeight(tooltipWeight);
@@ -80,6 +96,8 @@ const TreeMapView = (props) => {
return specBuilder.buildSpec(width);
}, [
width,
+ hierarchy,
+ flatType,
advancedMode,
advancedModeSpec,
tooltip,
@@ -108,9 +126,11 @@ const TreeMapView = (props) => {
};
TreeMapView.propTypes = {
- field: fieldPropTypes.isRequired,
- resource: PropTypes.object.isRequired,
+ field: fieldPropTypes,
+ resource: PropTypes.object,
data: PropTypes.any,
+ hierarchy: PropTypes.bool,
+ flatType: PropTypes.oneOf(['id/value', 'source/target/weight']),
advancedMode: PropTypes.bool,
advancedModeSpec: PropTypes.string,
tooltip: PropTypes.bool,
diff --git a/src/app/js/formats/vega/models/TreeMap.js b/src/app/js/formats/vega/models/TreeMap.js
index 6d8084b2c..f29292b8c 100644
--- a/src/app/js/formats/vega/models/TreeMap.js
+++ b/src/app/js/formats/vega/models/TreeMap.js
@@ -9,17 +9,23 @@ class TreeMap extends BasicChartVG {
constructor() {
super();
this.model = deepClone(treeMapModel);
+ this.hierarchy = true;
this.layout = 'squarify';
this.ratio = 2.0;
this.colors = MULTICHROMATIC_DEFAULT_COLORSET_STREAMGRAPH.split(' ');
this.tooltip = {
toggle: false,
+ third: true,
source: 'Source',
target: 'Target',
weight: 'Weight',
};
}
+ setHierarchy(bool) {
+ this.hierarchy = bool;
+ }
+
setLayout(newLayout) {
if (!TREE_MAP_LAYOUT.includes(newLayout)) {
return;
@@ -46,6 +52,10 @@ class TreeMap extends BasicChartVG {
this.tooltip.toggle = bool;
}
+ setThirdTooltip(bool) {
+ this.tooltip.third = bool;
+ }
+
/**
* Set the display name of the source
* @param title new name
@@ -96,16 +106,46 @@ class TreeMap extends BasicChartVG {
markEntry.type === 'rect' &&
markEntry.from.data === 'leaves'
) {
- const signal = ['{'];
- signal.push(`"${this.tooltip.source}": datum.hierarchy`);
- signal.push(',');
- signal.push(`"${this.tooltip.target}": datum.name`);
- signal.push(',');
- signal.push(`"${this.tooltip.weight}": datum.size`);
- signal.push('}');
- markEntry.encode.enter.tooltip = {
- signal: signal.join(''),
- };
+ if (this.hierarchy) {
+ const signal = ['{'];
+ signal.push(
+ `"${this.tooltip.source}": datum.hierarchy`,
+ );
+ signal.push(',');
+ signal.push(`"${this.tooltip.target}": datum.name`);
+ signal.push(',');
+ signal.push(`"${this.tooltip.weight}": datum.size`);
+ signal.push('}');
+ markEntry.encode.enter.tooltip = {
+ signal: signal.join(''),
+ };
+ } else {
+ if (this.tooltip.third) {
+ const signal = ['{'];
+ signal.push(
+ `"${this.tooltip.source}": datum.hierarchy.source`,
+ );
+ signal.push(',');
+ signal.push(
+ `"${this.tooltip.target}": datum.hierarchy.target`,
+ );
+ signal.push(',');
+ signal.push(`"${this.tooltip.weight}": datum.size`);
+ signal.push('}');
+ markEntry.encode.enter.tooltip = {
+ signal: signal.join(''),
+ };
+ } else {
+ const signal = ['{'];
+ signal.push(`"${this.tooltip.source}": datum.name`);
+ signal.push(',');
+ signal.push(`"${this.tooltip.target}": datum.size`);
+ signal.push('}');
+ markEntry.encode.enter.tooltip = {
+ signal: signal.join(''),
+ };
+ }
+ }
}
});
}
@@ -116,6 +156,14 @@ class TreeMap extends BasicChartVG {
}
});
+ if (!this.hierarchy && !this.tooltip.third) {
+ this.model.marks.forEach((markEntry) => {
+ if (markEntry.type === 'text') {
+ markEntry.from.data = 'leaves';
+ }
+ });
+ }
+
return this.model;
}
}