From 5388a890af7f4a2a19a228705ecacf4077be06d5 Mon Sep 17 00:00:00 2001 From: Daniel Quathamer Date: Mon, 21 Sep 2026 07:58:57 +0200 Subject: [PATCH] Sankey chart mit Echarts #17 --- src-modules/module/viz/conf/viz.xml | 3 + .../viz_properties_echarts_fuellen.sql | 53 +++- .../viz_properties_echarts_sankey_fuellen.sql | 13 +- .../module/viz/upgrade/viz_upgrade_man.sql | 3 + superx/xml/js/viz/viz_viewer.js | 299 +++++++----------- 5 files changed, 187 insertions(+), 184 deletions(-) diff --git a/src-modules/module/viz/conf/viz.xml b/src-modules/module/viz/conf/viz.xml index 0e03573..09ffbdd 100644 --- a/src-modules/module/viz/conf/viz.xml +++ b/src-modules/module/viz/conf/viz.xml @@ -1181,6 +1181,8 @@ detail_chart_uniquename varchar(255) + + @@ -1373,6 +1375,7 @@ detail_chart_uniquename varchar(255) + diff --git a/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_fuellen.sql b/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_fuellen.sql index 7098f07..c588ae3 100644 --- a/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_fuellen.sql +++ b/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_fuellen.sql @@ -758,7 +758,7 @@ "parent_property_uniquename":"valueLabelNode", "variableName":"distance_y" }, -{ "caption":"Ausrichtung Wertelabel (vertikal)", +{ "caption":"Ausrichtung Wertelabel", "prop_uniquename":"position_valueLabel", "explanation":"", "prop_default":"inside", @@ -809,6 +809,57 @@ "parent_property_uniquename":"valueLabelNode", "variableName":"offset_y_valueLabel" }, +{ "caption":"Wertelabel Schriftart", +"prop_uniquename":"valueLabelFontFamily", +"explanation":"Wertelabel Schriftart", +"prop_default":"sans-serif", +"prop_unit":"", +"is_generic":"1", +"static_values":"", +"is_mandatory":"0", +"input_type_uniquename":"TEXT", +"property_group_uniquename":"", +"sortnr":"460", +"range_from":"", +"range_to":"", +"prop_value_type":"STRING", +"parent_property_uniquename":"valueLabelNode", +"variableName":"valueLabelFontFamily" +}, +{ "caption":"Wertelabel Schriftgröße", +"prop_uniquename":"valueLabelFontSize", +"explanation":"Wertelabel Schriftgröße", +"prop_default":"1em", +"prop_unit":"", +"is_generic":"1", +"static_values":"", +"is_mandatory":"0", +"input_type_uniquename":"TEXT", +"property_group_uniquename":"", +"sortnr":"460", +"range_from":"", +"range_to":"", +"prop_value_type":"STRING", +"parent_property_uniquename":"valueLabelNode", +"variableName":"valueLabelFontSize" +}, +{ "caption":"Wertelabel Schriftfarbe", +"prop_uniquename":"valueLabelFontColor", +"explanation":"Wertelabel Schriftfarbe", +"prop_default":"1em", +"prop_unit":"", +"is_generic":"1", +"static_values":"", +"is_mandatory":"0", +"input_type_uniquename":"COLOR", +"property_group_uniquename":"", +"sortnr":"480", +"range_from":"", +"range_to":"", +"prop_value_type":"STRING", +"parent_property_uniquename":"valueLabelNode", +"variableName":"valueLabelFontColor" +}, { "caption":"Diagrammelement", "prop_uniquename":"seriesNode", "explanation":"Layout der Diagrammelemente", diff --git a/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_sankey_fuellen.sql b/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_sankey_fuellen.sql index b19a26a..4d6a7c2 100644 --- a/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_sankey_fuellen.sql +++ b/src-modules/module/viz/schluesseltabellen/viz_properties_echarts_sankey_fuellen.sql @@ -23,7 +23,18 @@ "is_mandatory":"1"}, {"viz_property_uniquename":"measure1", "is_mandatory":"1"}, -{"viz_property_uniquename":"nodeGap"} +{"viz_property_uniquename":"null_value_mask", +"is_mandatory":"0"}, +{"viz_property_uniquename":"margin.left"}, +{"viz_property_uniquename":"margin.top"}, +{"viz_property_uniquename":"margin.right"}, +{"viz_property_uniquename":"margin.bottom"}, +{"viz_property_uniquename":"valueLabelShow"}, +{"viz_property_uniquename":"valueLabelFontFamily"}, +{"viz_property_uniquename":"valueLabelFontColor"}, +{"viz_property_uniquename":"valueLabelFontSize"}, +{"viz_property_uniquename":"position_valueLabel"} + ] /> diff --git a/src-modules/module/viz/upgrade/viz_upgrade_man.sql b/src-modules/module/viz/upgrade/viz_upgrade_man.sql index 3374322..b8f6cc0 100644 --- a/src-modules/module/viz/upgrade/viz_upgrade_man.sql +++ b/src-modules/module/viz/upgrade/viz_upgrade_man.sql @@ -59,3 +59,6 @@ where T.uniquename in ('echarts_bar_x_series','echarts_bar_y_series','echarts_li delete from viz_type where uniquename in ('echarts_bar_x_series','echarts_bar_y_series','echarts_line_x_series','echarts_line_y_series'); +--Sankey nicht mehr über d3js, sondern Echarts: +delete from viz_chart where is_custom=0 +and uniquename='ERFOLG_BA_MA_SANKEY'; diff --git a/superx/xml/js/viz/viz_viewer.js b/superx/xml/js/viz/viz_viewer.js index d62cd7b..dcc0444 100644 --- a/superx/xml/js/viz/viz_viewer.js +++ b/superx/xml/js/viz/viz_viewer.js @@ -190,7 +190,10 @@ function fillEchartsDataSet(myRs,myRsMetaData,currentChartModel,seriesDistinctCo break; case "echarts_generic": dataset=fillEchartsDataSetSimpleCopy(myRs,myRsMetaData); - break; + break; + case "echarts_sankey": + dataset=myRs; + break; default: //dataset=fillEchartsDataSetSimpleCopy(myRs,myRsMetaData); var seriesFldname=getChartElementPropertyValueFromModel(currentChartModel,"1","stroke"); @@ -1082,6 +1085,7 @@ renderSankey(myCommonChartProperties,mySvg,sNodes,sLinks); function getSankeyNodes(data,captionEmptyTarget) { + //obsolet var myNodes=[]; var distinctSource = []; @@ -1114,6 +1118,7 @@ return myNodes; function getSankeyLinks(myNodes,data) { + //obsolet var myLinks=[]; var linkIndex=0; @@ -1136,6 +1141,7 @@ return myLinks; } function getSankeyNodeIndex(myNodes,name) { + //obsolet var myIndex=0; for (var i = 0; i < myNodes.length; i++) { @@ -1185,6 +1191,7 @@ function getMetadataOfVizTypeProperty(metaData,vizTypePropertyUniquename) } function renderSankey(myCommonChartProperties,svg,sNodes,sLinks) { + //obsolet // set the dimensions and margins of the graph var margin = { top: 10, right: 10, bottom: 10, left: 10 }, //var margin = { top: 0, right: 50, bottom: 0, left: 0 }, @@ -2500,226 +2507,154 @@ function getEchartOptionsSankey(currentChartModel,myDataset) var measure1Fldname=getChartElementPropertyValueFromModel(currentChartModel,"1","measure1"); var dimension1Fldname=getChartElementPropertyValueFromModel(currentChartModel,"1","dimension1"); var dimension2Fldname=getChartElementPropertyValueFromModel(currentChartModel,"1","dimension2"); + var titleText=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"caption"); var subtitleText=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"subtitle"); var scheme = getChartPropertyValue(currentChartModel.chartPropertiesUsed,"schemeArray"); var myPalette=getPaletteArray(scheme,""); +var captionEmptyNode=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"null_value_mask"); +if(captionEmptyNode=="") + captionEmptyNode="Leer"; -/*const sourceData = [ - {"rownr":1,"pj_ba_str":"2023","pj_ba_sem_von_str":"WiSe 2022/2023","pj_ba_sem_bis_str":"SoSe 2023","ba_abschluss_grp_str":"Bachelor","ba_abschluesse_summe":1964,"ma_sem_von_str":"WiSe 2022/2023","ma_sem_bis_str":"WiSe 2024/2025","ma_abschluss_grp_str":"LA Master","ma_anfaenger_summe":2,"ba_ma_ueberg_quote":0}, - {"rownr":2,"pj_ba_str":"2023","pj_ba_sem_von_str":"WiSe 2022/2023","pj_ba_sem_bis_str":"SoSe 2023","ba_abschluss_grp_str":"Bachelor","ba_abschluesse_summe":1964,"ma_sem_von_str":"WiSe 2022/2023","ma_sem_bis_str":"WiSe 2024/2025","ma_abschluss_grp_str":"Master","ma_anfaenger_summe":706,"ba_ma_ueberg_quote":0.36}, - {"rownr":3,"pj_ba_str":"2023","pj_ba_sem_von_str":"WiSe 2022/2023","pj_ba_sem_bis_str":"SoSe 2023","ba_abschluss_grp_str":"Bachelor","ba_abschluesse_summe":1964,"ma_sem_von_str":"WiSe 2022/2023","ma_sem_bis_str":"WiSe 2024/2025","ma_abschluss_grp_str":"kein Master","ma_anfaenger_summe":1256,"ba_ma_ueberg_quote":0.64}, - {"rownr":4,"pj_ba_str":"2023","pj_ba_sem_von_str":"WiSe 2022/2023","pj_ba_sem_bis_str":"SoSe 2023","ba_abschluss_grp_str":"LA Bachelor","ba_abschluesse_summe":1392,"ma_sem_von_str":"WiSe 2022/2023","ma_sem_bis_str":"WiSe 2024/2025","ma_abschluss_grp_str":"LA Master","ma_anfaenger_summe":1258,"ba_ma_ueberg_quote":0.9}, - {"rownr":5,"pj_ba_str":"2023","pj_ba_sem_von_str":"WiSe 2022/2023","pj_ba_sem_bis_str":"SoSe 2023","ba_abschluss_grp_str":"LA Bachelor","ba_abschluesse_summe":1392,"ma_sem_von_str":"WiSe 2022/2023","ma_sem_bis_str":"WiSe 2024/2025","ma_abschluss_grp_str":"Master","ma_anfaenger_summe":21,"ba_ma_ueberg_quote":0.02}, - {"rownr":6,"pj_ba_str":"2023","pj_ba_sem_von_str":"WiSe 2022/2023","pj_ba_sem_bis_str":"SoSe 2023","ba_abschluss_grp_str":"LA Bachelor","ba_abschluesse_summe":1392,"ma_sem_von_str":"WiSe 2022/2023","ma_sem_bis_str":"WiSe 2024/2025","ma_abschluss_grp_str":"kein Master","ma_anfaenger_summe":113,"ba_ma_ueberg_quote":0.08} -];*/ -const sourceData =myDataset.source; - -// DEBUG: Zeige die Rohdaten an -console.log('DATEN:', sourceData); - -// Farbzuordnung (intensivere Töne für besseren Kontrast) -const colorMap = { - 'Bachelor': '#3a5a2c', - 'LA Bachelor': '#8a9e7a', - 'Master': '#001e30', - 'LA Master': '#5a7a90', - 'kein Master': '#c0c8d0' -}; +var marginLeft=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"margin.left"); +var marginRight=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"margin.right"); +var marginTop=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"margin.top"); +var marginBottom=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"margin.bottom"); -// ✅ Erstelle ein Map für die Gesamtwerte der Quellknoten (ba_abschluesse_summe) -const sourceTotalMap = new Map(); -sourceData.forEach(row => { - const sourceName = row.ba_abschluss_grp_str; - const total = row.ba_abschluesse_summe; +var valueLabelShow=(getChartPropertyValue(currentChartModel.chartPropertiesUsed,"valueLabelShow")=="true")?true:false; +var valueLabelFontColor=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"valueLabelFontColor"); +var valueLabelFontSize=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"valueLabelFontSize"); +var valueLabelFontFamily=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"valueLabelFontFamily"); +var position_valueLabel=getChartPropertyValue(currentChartModel.chartPropertiesUsed,"position_valueLabel"); - if (!sourceTotalMap.has(sourceName)) { - sourceTotalMap.set(sourceName, total); - } else if (sourceTotalMap.get(sourceName) !== total) { - console.warn(`⚠️ Inkonsequenter Gesamtwert für Quellknoten "${sourceName}": ${total} vs ${sourceTotalMap.get(sourceName)}`); - } -}); -// DEBUG: Zeige die Quellgesamtwerte an -console.log('Gesamtwerte der Quellknoten (ba_abschluesse_summe):', Object.fromEntries(sourceTotalMap)); +var dataNodeArray=new Array(); +dataNodeArray=getDistinctColumnValuesAsSankeyNodes(myDataset,dimension1Fldname,dataNodeArray,captionEmptyNode); +dataNodeArray=getDistinctColumnValuesAsSankeyNodes(myDataset,dimension2Fldname,dataNodeArray,captionEmptyNode); -// ✅ Generiere die Links mit Farbverlauf (Gradienten) -const links = sourceData.map(row => { - const source = row.ba_abschluss_grp_str; - const target = row.ma_abschluss_grp_str; - const value = row.ma_anfaenger_summe; - const sourceColor = colorMap[source]; - const targetColor = colorMap[target]; +var linkArray=new Array(); - return { - source, - target, - value, - lineStyle: { - width: 2, - color: echartsSankeyCreateGradientColor(sourceColor, targetColor), - opacity: 0.3 - } +myDataset.forEach(row => { + var sourceName = row[dimension1Fldname].toString(); + if(sourceName==""){ + sourceName=captionEmptyNode; + } + var targetName = row[dimension2Fldname].toString(); + if(targetName==""){ + targetName=captionEmptyNode; + } + var linkValue=row[measure1Fldname]; + + + var linkObj={source: sourceName, + target:targetName, + value: linkValue }; -}); - -// DEBUG: Zeige die generierten Links an -console.log('GENERIERTE LINKS mit Gradienten:', links); - + linkArray.push(linkObj); + + }); + // ECharts Option var option = { title: { - text: 'Übergang von Bachelor zu Master', + text: titleText, left: 'center', - textStyle: { fontSize: 16 } + textStyle: { fontSize: "2em" } }, tooltip: { trigger: 'item', triggerOn: 'mousemove', formatter: function (params) { - if (params.dataType === 'node') { - const nodeName = params.name; - - // Knotenfarbe als Tooltip-Farbe - const color = colorMap[nodeName] || '#ccc'; - - // Prüfe, ob der Knoten als Quelle oder Ziel vorkommt - const isSource = links.some(link => link.source === nodeName); - const isTarget = links.some(link => link.target === nodeName); - - if (isSource && !isTarget) { - // Nur Quelle (links): Name + ba_abschluesse_summe - const total = sourceTotalMap.get(nodeName); - return `${nodeName}-Abschlüsse: ${total}`; - } else if (isTarget) { - // Ziel (rechts): Name + summierte ankommende Werte - const incomingValue = links - .filter(link => link.target === nodeName) - .reduce((sum, link) => sum + link.value, 0); - return `${nodeName}-Studiengänge: ${incomingValue}`; - } else { - return nodeName; - } - } else { - // Link-Tooltip - return params.data.source + ' --> ' + params.data.target + ': ' + params.data.value; - } - } - }, - series: { - name: 'Bachelor-Master Übergänge', + if (params.dataType === 'node') { + var nodeName = params.name; + return nodeName + ': ' + params.value.toLocaleString(); + + } + else + { //link: + return params.data.source + ' --> ' + params.data.target + ': ' + params.data.value.toLocaleString(); + } + } + }, + series: { + name: titleText, type: 'sankey', - layout: 'none', - nodeGap: 25, // ← Erhöhter vertikaler Abstand - nodeWidth: 60, // ← Breite der Knoten + layout: 'center', + //layoutIterations: 0, // Um automatische Sortierung zu deaktivieren + //nodeOrder: ['Bachelor', 'Kein Master BUW','Master'] ,//'LA Bachelor', 'kein Bachelor an UzK', 'Master', 'LA Master', 'kein Master an UzK'], + nodeAlign: 'left', + nodeGap: 25, // Erhöhter vertikaler Abstand + nodeWidth: 60, // Breite der Knoten + edgeLabel:{ show: valueLabelShow, + fontFamily:valueLabelFontFamily, + color:valueLabelFontColor, + fontSize:valueLabelFontSize, + formatter: function (params) { + return params.value.toLocaleString() + } + }, emphasis: { - focus: 'adjacency' + disabled:true + //focus: 'self' //'adjacency' }, - data: [ - { - name: 'Bachelor', - itemStyle: { color: colorMap['Bachelor'] }, - label: { position: 'left', show: true, fontSize: 12, color: '#333' } - }, - { - name: 'LA Bachelor', - itemStyle: { color: colorMap['LA Bachelor'] }, - label: { position: 'left', show: true, fontSize: 12, color: '#333' } - }, - { - name: 'Master', - itemStyle: { color: colorMap['Master'] }, - label: { position: 'right', show: true, fontSize: 12, color: '#333' } - }, - { - name: 'LA Master', - itemStyle: { color: colorMap['LA Master'] }, - label: { position: 'right', show: true, fontSize: 12, color: '#333' } - }, - { - name: 'kein Master', - itemStyle: { color: colorMap['kein Master'] }, - label: { position: 'right', show: true, fontSize: 12, color: '#333' } - } - ], - links: links, + data: dataNodeArray, + links: linkArray, label: { show: true, - fontSize: 12, - color: '#333' + fontFamily:valueLabelFontFamily, + color:valueLabelFontColor, + fontSize:"1em", + position:position_valueLabel, + distance:1 }, lineStyle: { width: 2, - color: 'source' + color: "gradient" // , opacity: 0.9, // curveness: 0.3 - } - } -}; -var option2 = { - dataset: { - source: myDataset - }, - title: { - text: titleText, - subtext: subtitleText, - left: getChartPropertyValue(currentChartModel.chartPropertiesUsed,"title.left"), //"center", - top: getChartPropertyValue(currentChartModel.chartPropertiesUsed,"title.top"), //"top", - textStyle: { - fontSize: 20 }, - subtextStyle: { - fontSize: 15 - } - }, - - series: [ - { - type: 'pie',// sunburst - /*encode: { - value: measure1Fldname, - name: dimension1Fldname - },*/ - tooltip: { - trigger: "item" - }, - stillShowZeroSum: false, - - label: { - show: (getChartPropertyValue(currentChartModel.chartPropertiesUsed,"valueLabelShow")=="true")?true:false, //true - fontWeight:'bolder', + left:marginLeft, + top:marginTop, + right:marginRight, + bottom:marginBottom, + width:"100%" - formatter: '{c}' - - } , - - fontSize: 12, - radius: [getChartPropertyValue(currentChartModel.chartPropertiesUsed,"innerRadius"),getChartPropertyValue(currentChartModel.chartPropertiesUsed,"outerRadius")],// ['30%', '80%'], //1. Parameter inside radius, macht ihn zum Donut. 2.Parameter definiert den Anteil des Kreises am ganzen Viewport" - color: myPalette, - itemStyle: { - borderRadius: (getChartPropertyValue(currentChartModel.chartPropertiesUsed,"cornerRadiusRounded")=="true" ? [20, 5, 5, 10] :[]), - borderColor: '#fff', - borderWidth: 2 - } - } - ] + } }; return option; } -// ✅ Funktion: Erzeugt einen Farbverlauf von sourceColor zu targetColor -function echartsSankeyCreateGradientColor(sourceColor, targetColor) { - return { - type: 'linear', - x: 0, y: 0, x2: 1, y2: 0, - colorStops: [ - { offset: 0, color: sourceColor }, - { offset: 1, color: targetColor } - ] - }; +function getDistinctColumnValuesAsSankeyNodes(myRs,fieldname,nodes,captionEmptyNode) +{ + var distinctValues=new Array(); + + myRs.forEach(row => { + var newName = row[fieldname].toString(); + if(newName=="") + newName=captionEmptyNode; + + if( distinctValues.length==0) + { + distinctValues.push(newName); + } + else if( distinctValues.length>0 && !distinctValues.includes(newName)) + { + + distinctValues.push(newName); + + } + + + + }); + distinctValues.forEach(row => { + var nameObj={name:row}; + nodes.push(nameObj); + }); + return nodes; }