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 ? ( + + _id / value + + source / target / weight + + + ) : 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; } }