diff --git a/src/app/custom/translations.tsv b/src/app/custom/translations.tsv index 84d63585c..8ee8c966f 100644 --- a/src/app/custom/translations.tsv +++ b/src/app/custom/translations.tsv @@ -1102,3 +1102,5 @@ "routine_arg" "Field N°" "Champ N°" "routine_arg_add" "Add a new field" "Ajouter un nouveau champ" "routine_arg_delete" "Delete this field" "Supprimer ce champ" +"treemap_hierarchy_data" "Hierarchy data" "Données hiérarchiques" +"treemap_flat_data_type" "Data structure" "Structure 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 65cec779f..db01ae064 100644 --- a/src/app/js/formats/utils/components/field-set/FormatFieldSetPreview.js +++ b/src/app/js/formats/utils/components/field-set/FormatFieldSetPreview.js @@ -22,16 +22,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 b0e449aef..15042bb8a 100644 --- a/src/app/js/formats/vega/component/tree-map/TreeMapAdmin.js +++ b/src/app/js/formats/vega/component/tree-map/TreeMapAdmin.js @@ -23,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'; @@ -34,6 +38,8 @@ export const defaultArgs = { maxSize: 5, orderBy: 'value/asc', }, + hierarchy: true, + flatType: 'id/value', advancedMode: false, advancedModeSpec: null, tooltip: false, @@ -57,6 +63,8 @@ const TreeMapAdmin = (props) => { } = props; const { + hierarchy, + flatType, advancedMode, advancedModeSpec, tooltip, @@ -69,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]); @@ -84,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); @@ -110,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 = () => { @@ -178,6 +208,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} /> @@ -256,7 +314,7 @@ const TreeMapAdmin = (props) => { @@ -271,6 +329,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..f72cea62e 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} + */ + isHierarchy; + /** + * @type {Map} + */ + originalObject; + + /** + * @param {Array<{source: string, target: string, weight: string | number, original?: any}>} data + * @param isHierarchy + */ + constructor(data, isHierarchy = true) { this.data = data; this.idIncrement = 0; this.ids = new Map(); this.rawNodesAndLeaves = new Map(); this.formattedNodesAndLeaves = new Map(); this.filteredNodes = new Set(); + this.isHierarchy = isHierarchy; + + if (!this.isHierarchy) { + this.originalObject = new Map(); + data.forEach((datum) => { + if (datum.original) { + this.originalObject.set(datum.target, datum.original); + } + }); + } } /** @@ -141,7 +165,11 @@ export default class TreeMapData { continue; } - datum.hierarchy = this.createHierarchy(datum.parent); + if (this.isHierarchy) { + datum.hierarchy = this.createHierarchy(datum.parent); + } else { + datum.original = this.originalObject.get(datum.name); + } transformedAndCleanupData.push(datum); } @@ -159,3 +187,49 @@ export default class TreeMapData { return this.buildReturnable(); } } + +/** + * @param {Array<{_id: string, value: string | number}>} values + * @return {Array<{source: string, target: string, weight: string | number, original: any}>} + */ +TreeMapData.transformIdValue = (values) => { + const finalValues = []; + values.forEach((datum) => { + const middleNode = `root_${datum._id}`; + finalValues.push({ + source: middleNode, + target: datum._id, + weight: datum.value, + original: { ...datum }, + }); + 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, original: any}>} + */ +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, + original: { ...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..6d9feab6a 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.isHierarchy = 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.isHierarchy = 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.isHierarchy) { + 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.original.source`, + ); + signal.push(','); + signal.push( + `"${this.tooltip.target}": datum.original.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.isHierarchy && !this.tooltip.third) { + this.model.marks.forEach((markEntry) => { + if (markEntry.type === 'text') { + markEntry.from.data = 'leaves'; + } + }); + } + return this.model; } }