({ runs, valueKey, label, formatter, min = null, max = null })
| 938 | } |
| 939 | |
| 940 | function compareLineChart({ runs, valueKey, label, formatter, min = null, max = null }) { |
| 941 | const datasets = runs.map((run, index) => { |
| 942 | const points = (run.series || []) |
| 943 | .map((point, pointIndex) => ({ |
| 944 | label: point.label || point.minute || String(pointIndex + 1), |
| 945 | value: toNumber(point[valueKey]), |
| 946 | })) |
| 947 | .filter((point) => point.value !== null); |
| 948 | return { |
| 949 | name: run.name, |
| 950 | color: index === 0 ? "#0979c6" : "#8b9bb1", |
| 951 | points, |
| 952 | }; |
| 953 | }); |
| 954 | |
| 955 | if (datasets.some((dataset) => dataset.points.length < 2)) { |
| 956 | return emptyBlock(`Both reports need timeline data to compare ${label.toLowerCase()}.`); |
| 957 | } |
| 958 | |
| 959 | const width = 620; |
| 960 | const height = 220; |
| 961 | const padding = 24; |
| 962 | const allValues = datasets.flatMap((dataset) => dataset.points.map((point) => point.value)); |
| 963 | const yMin = min !== null ? min : Math.min(...allValues); |
| 964 | const yMax = max !== null ? max : Math.max(...allValues); |
| 965 | const yRange = Math.max(yMax - yMin, 0.0001); |
| 966 | |
| 967 | const polylines = datasets |
| 968 | .map((dataset) => { |
| 969 | const xStep = (width - padding * 2) / Math.max(dataset.points.length - 1, 1); |
| 970 | const plotted = dataset.points.map((point, index) => { |
| 971 | const x = padding + index * xStep; |
| 972 | const y = height - padding - ((point.value - yMin) / yRange) * (height - padding * 2); |
| 973 | return { ...point, x, y }; |
| 974 | }); |
| 975 | return { |
| 976 | ...dataset, |
| 977 | plotted, |
| 978 | polyline: plotted.map((point) => `${point.x},${point.y}`).join(" "), |
| 979 | }; |
| 980 | }); |
| 981 | |
| 982 | return ` |
| 983 | <svg class="svg-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(label)} comparison"> |
| 984 | <line x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}" stroke="currentColor" opacity="0.15"></line> |
| 985 | ${polylines |
| 986 | .map( |
| 987 | (dataset) => ` |
| 988 | <polyline points="${dataset.polyline}" fill="none" stroke="${dataset.color}" stroke-width="3"></polyline> |
| 989 | ${dataset.plotted |
| 990 | .map( |
| 991 | (point) => `<circle cx="${point.x}" cy="${point.y}" r="3" fill="${dataset.color}"><title>${escapeHtml(dataset.name)} - ${escapeHtml(point.label)}: ${escapeHtml(formatter(point.value))}</title></circle>` |
| 992 | ) |
| 993 | .join("")} |
| 994 | ` |
| 995 | ) |
| 996 | .join("")} |
| 997 | </svg> |
no test coverage detected