|
904 | 904 | } |
905 | 905 | } |
906 | 906 |
|
| 907 | + // Renders a stacked area chart where each series fills from the previous, |
| 908 | + // showing both individual contributions and total composition. |
| 909 | + function renderStackedAreaGraph(parent, name, stackedItems, fullKey) { |
| 910 | + const isDark = document.body.classList.contains("dark-theme"); |
| 911 | + const textColor = isDark ? "#e0e0e0" : "#4a4a4a"; |
| 912 | + |
| 913 | + let grid = parent.querySelector(".benchmark-graphs"); |
| 914 | + if (!grid) { |
| 915 | + grid = document.createElement("div"); |
| 916 | + grid.className = "benchmark-graphs"; |
| 917 | + parent.appendChild(grid); |
| 918 | + } |
| 919 | + const container = document.createElement("div"); |
| 920 | + container.className = "chart-container"; |
| 921 | + container.style.gridColumn = "1 / -1"; |
| 922 | + container.style.minHeight = "400px"; |
| 923 | + grid.appendChild(container); |
| 924 | + |
| 925 | + const canvas = document.createElement("canvas"); |
| 926 | + canvas.className = "benchmark-chart"; |
| 927 | + container.appendChild(canvas); |
| 928 | + |
| 929 | + // Collect all unique commits across all component series |
| 930 | + const commitOrder = []; |
| 931 | + const commitSet = new Set(); |
| 932 | + const commitInfoMap = new Map(); |
| 933 | + stackedItems.forEach((item) => { |
| 934 | + item.benches.forEach((entry) => { |
| 935 | + if (!commitSet.has(entry.commit.id)) { |
| 936 | + commitSet.add(entry.commit.id); |
| 937 | + commitOrder.push(entry.commit.id); |
| 938 | + commitInfoMap.set(entry.commit.id, entry.commit); |
| 939 | + } |
| 940 | + }); |
| 941 | + }); |
| 942 | + |
| 943 | + const labels = commitOrder.map((id) => id.slice(0, 7)); |
| 944 | + |
| 945 | + // Stacked area colors (semi-opaque for fill) |
| 946 | + const colors = [ |
| 947 | + "#e24a4a", "#4a90e2", "#2ecc71", "#e2c94a", "#9b59b6", |
| 948 | + "#1abc9c", "#e67e22", "#3498db", "#e74c3c", "#27ae60", |
| 949 | + "#f39c12", "#8e44ad", "#16a085", "#d35400", "#2980b9", |
| 950 | + "#c0392b", "#f1c40f", "#7d3c98", "#148f77", "#d68910", |
| 951 | + ]; |
| 952 | + |
| 953 | + // Sort alphabetically (largest categories tend to be named first) |
| 954 | + const sorted = [...stackedItems].sort((a, b) => { |
| 955 | + const aName = a.fullKey.split("/").pop(); |
| 956 | + const bName = b.fullKey.split("/").pop(); |
| 957 | + return aName.localeCompare(bName); |
| 958 | + }); |
| 959 | + |
| 960 | + const datasets = sorted.map((item, i) => { |
| 961 | + const color = colors[i % colors.length]; |
| 962 | + const metricName = item.fullKey.split("/").pop(); |
| 963 | + const valueMap = new Map(); |
| 964 | + item.benches.forEach((entry) => { |
| 965 | + valueMap.set(entry.commit.id, entry.bench.value); |
| 966 | + }); |
| 967 | + const data = commitOrder.map((id) => valueMap.get(id) ?? 0); |
| 968 | + return { |
| 969 | + label: metricName, |
| 970 | + data, |
| 971 | + borderColor: color, |
| 972 | + backgroundColor: color + "80", |
| 973 | + borderWidth: 1, |
| 974 | + pointRadius: 1, |
| 975 | + fill: true, |
| 976 | + }; |
| 977 | + }); |
| 978 | + |
| 979 | + const unit = stackedItems[0]?.benches[0]?.bench?.unit || "MB"; |
| 980 | + |
| 981 | + const chart = new Chart(canvas, { |
| 982 | + type: "line", |
| 983 | + data: { labels, datasets }, |
| 984 | + options: { |
| 985 | + responsive: true, |
| 986 | + title: { |
| 987 | + display: true, |
| 988 | + text: name, |
| 989 | + fontColor: textColor, |
| 990 | + fontSize: 14, |
| 991 | + }, |
| 992 | + legend: { |
| 993 | + display: true, |
| 994 | + position: "bottom", |
| 995 | + labels: { |
| 996 | + fontColor: textColor, |
| 997 | + fontSize: 11, |
| 998 | + padding: 12, |
| 999 | + usePointStyle: true, |
| 1000 | + }, |
| 1001 | + }, |
| 1002 | + scales: { |
| 1003 | + xAxes: [ |
| 1004 | + { |
| 1005 | + scaleLabel: { |
| 1006 | + display: true, |
| 1007 | + labelString: "commit", |
| 1008 | + fontColor: textColor, |
| 1009 | + }, |
| 1010 | + ticks: { fontColor: textColor }, |
| 1011 | + }, |
| 1012 | + ], |
| 1013 | + yAxes: [ |
| 1014 | + { |
| 1015 | + stacked: true, |
| 1016 | + scaleLabel: { |
| 1017 | + display: true, |
| 1018 | + labelString: unit, |
| 1019 | + fontColor: textColor, |
| 1020 | + }, |
| 1021 | + ticks: { beginAtZero: true, fontColor: textColor }, |
| 1022 | + }, |
| 1023 | + ], |
| 1024 | + }, |
| 1025 | + tooltips: { |
| 1026 | + mode: "index", |
| 1027 | + intersect: false, |
| 1028 | + callbacks: { |
| 1029 | + title: (tooltipItems) => { |
| 1030 | + if (tooltipItems.length > 0) { |
| 1031 | + const idx = tooltipItems[0].index; |
| 1032 | + const commitId = commitOrder[idx]; |
| 1033 | + const commit = commitInfoMap.get(commitId); |
| 1034 | + return commit |
| 1035 | + ? commitId.slice(0, 7) + " - " + commit.message |
| 1036 | + : commitId.slice(0, 7); |
| 1037 | + } |
| 1038 | + return ""; |
| 1039 | + }, |
| 1040 | + label: (item) => { |
| 1041 | + const dsLabel = datasets[item.datasetIndex].label; |
| 1042 | + return " " + dsLabel + ": " + item.value + " " + unit; |
| 1043 | + }, |
| 1044 | + afterBody: (tooltipItems) => { |
| 1045 | + const total = tooltipItems.reduce( |
| 1046 | + (sum, item) => sum + parseFloat(item.value || 0), |
| 1047 | + 0 |
| 1048 | + ); |
| 1049 | + return " Total: " + total.toFixed(1) + " " + unit; |
| 1050 | + }, |
| 1051 | + }, |
| 1052 | + }, |
| 1053 | + onClick: (_mouseEvent, activeElems) => { |
| 1054 | + if (activeElems.length === 0) return; |
| 1055 | + const index = activeElems[0]._index; |
| 1056 | + const commitId = commitOrder[index]; |
| 1057 | + const commit = commitInfoMap.get(commitId); |
| 1058 | + if (commit && commit.url) { |
| 1059 | + window.open(commit.url, "_blank"); |
| 1060 | + } |
| 1061 | + }, |
| 1062 | + }, |
| 1063 | + }); |
| 1064 | + |
| 1065 | + window.chartInstances.push(chart); |
| 1066 | + if (fullKey) { |
| 1067 | + window.chartsByBenchName.set(fullKey, chart); |
| 1068 | + } |
| 1069 | + } |
| 1070 | + |
907 | 1071 | function renderBenchSet(name, benchSet, main) { |
908 | 1072 | const setElem = document.createElement("div"); |
909 | 1073 | setElem.className = "benchmark-set"; |
|
929 | 1093 | } |
930 | 1094 |
|
931 | 1095 | // Detect and consolidate stacked chart entries. |
932 | | - // Entries with extra: "stacked:GROUP_NAME" are grouped into a single stacked chart. |
| 1096 | + // Entries with extra: "stacked:GROUP_NAME" are grouped into overlaid line charts. |
| 1097 | + // Entries with extra: "stacked-area:GROUP_NAME" are grouped into stacked area charts. |
933 | 1098 | const stackedGroups = new Map(); |
| 1099 | + const stackedAreaGroups = new Map(); |
934 | 1100 | const regularItems = []; |
935 | 1101 | items.forEach((item) => { |
936 | 1102 | const latestBench = item.benches[item.benches.length - 1]?.bench; |
937 | 1103 | const extra = latestBench?.extra || ""; |
| 1104 | + const stackedAreaMatch = extra.match(/^stacked-area:(.+)$/); |
938 | 1105 | const stackedMatch = extra.match(/^stacked:(.+)$/); |
939 | | - if (stackedMatch) { |
| 1106 | + if (stackedAreaMatch) { |
| 1107 | + const groupName = stackedAreaMatch[1]; |
| 1108 | + if (!stackedAreaGroups.has(groupName)) { |
| 1109 | + stackedAreaGroups.set(groupName, []); |
| 1110 | + } |
| 1111 | + stackedAreaGroups.get(groupName).push(item); |
| 1112 | + } else if (stackedMatch) { |
940 | 1113 | const groupName = stackedMatch[1]; |
941 | 1114 | if (!stackedGroups.has(groupName)) { |
942 | 1115 | stackedGroups.set(groupName, []); |
|
947 | 1120 | } |
948 | 1121 | }); |
949 | 1122 |
|
950 | | - // Add consolidated stacked chart entries |
| 1123 | + // Add consolidated stacked line chart entries |
951 | 1124 | stackedGroups.forEach((groupItems, groupName) => { |
952 | 1125 | const group = groupItems[0].group; |
953 | 1126 | const parts = groupName.split("/"); |
|
961 | 1134 | }); |
962 | 1135 | }); |
963 | 1136 |
|
| 1137 | + // Add consolidated stacked area chart entries |
| 1138 | + stackedAreaGroups.forEach((groupItems, groupName) => { |
| 1139 | + const group = groupItems[0].group; |
| 1140 | + const parts = groupName.split("/"); |
| 1141 | + const chartName = parts[parts.length - 1]; |
| 1142 | + regularItems.push({ |
| 1143 | + group, |
| 1144 | + chartName, |
| 1145 | + benches: null, |
| 1146 | + fullKey: groupName, |
| 1147 | + stackedAreaItems: groupItems, |
| 1148 | + }); |
| 1149 | + }); |
| 1150 | + |
964 | 1151 | // Build hierarchical tree from group paths. |
965 | 1152 | const tree = new Map(); |
966 | 1153 | regularItems.forEach((item) => { |
|
976 | 1163 | benches: item.benches, |
977 | 1164 | fullKey: item.fullKey, |
978 | 1165 | stackedItems: item.stackedItems || null, |
| 1166 | + stackedAreaItems: item.stackedAreaItems || null, |
979 | 1167 | }); |
980 | 1168 | } |
981 | 1169 | current = current.get(part).children; |
|
1078 | 1266 |
|
1079 | 1267 | // Render charts for this node. |
1080 | 1268 | value.charts.forEach((item) => { |
1081 | | - if (item.stackedItems) { |
| 1269 | + if (item.stackedAreaItems) { |
| 1270 | + renderStackedAreaGraph(groupContent, item.chartName, item.stackedAreaItems, item.fullKey); |
| 1271 | + } else if (item.stackedItems) { |
1082 | 1272 | renderStackedGraph(groupContent, item.chartName, item.stackedItems, item.fullKey); |
1083 | 1273 | } else { |
1084 | 1274 | renderGraph(groupContent, item.chartName, item.benches, item.fullKey); |
|
0 commit comments