|  | 
| 34 | 34 |             margin: 0; | 
| 35 | 35 |         } | 
| 36 | 36 | 
 | 
|  | 37 | +        .brand-link { | 
|  | 38 | +            margin-top: 6px; | 
|  | 39 | +            font-size: 12px; | 
|  | 40 | +            letter-spacing: 0.08em; | 
|  | 41 | +        } | 
|  | 42 | + | 
|  | 43 | +        .brand-link a { | 
|  | 44 | +            color: #7f28ff; | 
|  | 45 | +            text-decoration: none; | 
|  | 46 | +        } | 
|  | 47 | + | 
|  | 48 | +        .brand-link a:hover, | 
|  | 49 | +        .brand-link a:focus { | 
|  | 50 | +            text-decoration: underline; | 
|  | 51 | +        } | 
|  | 52 | + | 
| 37 | 53 |         .info-grid { | 
| 38 | 54 |             display: grid; | 
| 39 |  | -            grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | 
| 40 |  | -            gap: 16px; | 
|  | 55 | +            grid-template-columns: repeat(6, minmax(0, 1fr)); | 
|  | 56 | +            gap: 12px; | 
| 41 | 57 |             margin-bottom: 32px; | 
| 42 | 58 |         } | 
| 43 | 59 | 
 | 
| 44 | 60 |         .info-card { | 
| 45 |  | -            background: linear-gradient(135deg, #7f28ff 0%, #c084fc 100%); | 
| 46 |  | -            border-radius: 12px; | 
| 47 |  | -            padding: 18px 20px; | 
| 48 |  | -            color: #ffffff; | 
| 49 |  | -            box-shadow: 0 6px 16px rgba(127, 40, 255, 0.2); | 
|  | 61 | +            background: #f7f5ff; | 
|  | 62 | +            border: 1px solid #d7caff; | 
|  | 63 | +            border-radius: 10px; | 
|  | 64 | +            padding: 16px 18px; | 
|  | 65 | +            color: #1a0a2e; | 
|  | 66 | +            box-shadow: 0 4px 10px rgba(127, 40, 255, 0.08); | 
| 50 | 67 |         } | 
| 51 | 68 | 
 | 
| 52 | 69 |         .info-label { | 
| 53 | 70 |             font-size: 12px; | 
| 54 | 71 |             letter-spacing: 0.06em; | 
| 55 | 72 |             text-transform: uppercase; | 
| 56 |  | -            color: #e9d5ff; | 
|  | 73 | +            color: #6b5b95; | 
| 57 | 74 |         } | 
| 58 | 75 | 
 | 
| 59 | 76 |         .info-value { | 
|  | 
| 74 | 91 | 
 | 
| 75 | 92 |         .summary-grid { | 
| 76 | 93 |             display: grid; | 
| 77 |  | -            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | 
|  | 94 | +            grid-template-columns: repeat(5, minmax(0, 1fr)); | 
| 78 | 95 |             gap: 16px; | 
| 79 | 96 |             margin-bottom: 32px; | 
| 80 | 97 |         } | 
|  | 
| 132 | 149 |             height: 340px; | 
| 133 | 150 |         } | 
| 134 | 151 | 
 | 
|  | 152 | +        .chart-legend { | 
|  | 153 | +            display: flex; | 
|  | 154 | +            flex-wrap: wrap; | 
|  | 155 | +            gap: 8px; | 
|  | 156 | +            margin-top: 12px; | 
|  | 157 | +        } | 
|  | 158 | + | 
|  | 159 | +        .chart-legend button { | 
|  | 160 | +            border: 1px solid #d7caff; | 
|  | 161 | +            border-radius: 16px; | 
|  | 162 | +            padding: 6px 12px; | 
|  | 163 | +            background: #f7f5ff; | 
|  | 164 | +            color: #4b3f6b; | 
|  | 165 | +            font-size: 12px; | 
|  | 166 | +            cursor: pointer; | 
|  | 167 | +            transition: all 0.2s ease; | 
|  | 168 | +        } | 
|  | 169 | + | 
|  | 170 | +        .chart-legend button.active { | 
|  | 171 | +            background: #7f28ff; | 
|  | 172 | +            color: #ffffff; | 
|  | 173 | +            border-color: #7f28ff; | 
|  | 174 | +            box-shadow: 0 2px 6px rgba(127, 40, 255, 0.25); | 
|  | 175 | +        } | 
|  | 176 | + | 
|  | 177 | +        .chart-legend button:hover, | 
|  | 178 | +        .chart-legend button:focus { | 
|  | 179 | +            border-color: #7f28ff; | 
|  | 180 | +        } | 
|  | 181 | + | 
| 135 | 182 |         .table-section { | 
| 136 | 183 |             margin-bottom: 40px; | 
| 137 | 184 |         } | 
|  | 
| 220 | 267 |             margin-bottom: 4px; | 
| 221 | 268 |         } | 
| 222 | 269 | 
 | 
|  | 270 | +        @media (max-width: 1400px) { | 
|  | 271 | +            .info-grid { | 
|  | 272 | +                grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | 
|  | 273 | +            } | 
|  | 274 | +            .summary-grid { | 
|  | 275 | +                grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | 
|  | 276 | +            } | 
|  | 277 | +        } | 
|  | 278 | + | 
| 223 | 279 |         @media print { | 
| 224 | 280 |             body { | 
| 225 | 281 |                 background: white; | 
|  | 
| 256 | 312 |         <div class="header"> | 
| 257 | 313 |             <img src="https://zeuz.ai/_next/image?url=%2Fold-assets%2FLogo.svg&w=384&q=75" alt="ZeuZ Logo"> | 
| 258 | 314 |             <h1>Performance Test Report</h1> | 
|  | 315 | +            <div class="brand-link"> | 
|  | 316 | +                <a href="https://zeuz.ai" target="_blank" rel="noopener">zeuz.ai</a> | 
|  | 317 | +            </div> | 
| 259 | 318 |         </div> | 
| 260 | 319 | 
 | 
| 261 | 320 |         <div class="info-grid"> | 
| @@ -334,42 +393,51 @@ <h2>Overall Summary</h2> | 
| 334 | 393 |         <div class="charts-grid"> | 
| 335 | 394 |             <div class="chart-card"> | 
| 336 | 395 |                 <div class="chart-title">Requests Per Second (Total)</div> | 
| 337 |  | -                <div id="overall_requests" class="chart-container"></div> | 
|  | 396 | +                <div id="overall_requests" class="chart-container" data-legend-target="legend-overall_requests"></div> | 
|  | 397 | +                <div class="chart-legend" id="legend-overall_requests"></div> | 
| 338 | 398 |             </div> | 
| 339 | 399 |             <div class="chart-card"> | 
| 340 | 400 |                 <div class="chart-title">Active Users (Estimated Concurrency)</div> | 
| 341 |  | -                <div id="overall_concurrency" class="chart-container"></div> | 
|  | 401 | +                <div id="overall_concurrency" class="chart-container" data-legend-target="legend-overall_concurrency"></div> | 
|  | 402 | +                <div class="chart-legend" id="legend-overall_concurrency"></div> | 
| 342 | 403 |             </div> | 
| 343 | 404 |             <div class="chart-card"> | 
| 344 | 405 |                 <div class="chart-title">Total Data Transferred Per Second</div> | 
| 345 |  | -                <div id="overall_throughput" class="chart-container"></div> | 
|  | 406 | +                <div id="overall_throughput" class="chart-container" data-legend-target="legend-overall_throughput"></div> | 
|  | 407 | +                <div class="chart-legend" id="legend-overall_throughput"></div> | 
| 346 | 408 |             </div> | 
| 347 | 409 |             <div class="chart-card"> | 
| 348 | 410 |                 <div class="chart-title">Error Rate (%) Per Second</div> | 
| 349 |  | -                <div id="overall_error_rate" class="chart-container"></div> | 
|  | 411 | +                <div id="overall_error_rate" class="chart-container" data-legend-target="legend-overall_error_rate"></div> | 
|  | 412 | +                <div class="chart-legend" id="legend-overall_error_rate"></div> | 
| 350 | 413 |             </div> | 
| 351 | 414 |         </div> | 
| 352 | 415 | 
 | 
| 353 | 416 |         <div class="charts-grid"> | 
| 354 | 417 |             <div class="chart-card"> | 
| 355 | 418 |                 <div class="chart-title">Average Response Time (Overall)</div> | 
| 356 |  | -                <div id="overall_avg_response" class="chart-container"></div> | 
|  | 419 | +                <div id="overall_avg_response" class="chart-container" data-legend-target="legend-overall_avg_response"></div> | 
|  | 420 | +                <div class="chart-legend" id="legend-overall_avg_response"></div> | 
| 357 | 421 |             </div> | 
| 358 | 422 |             <div class="chart-card"> | 
| 359 | 423 |                 <div class="chart-title">Latency Percentiles (Overall)</div> | 
| 360 |  | -                <div id="overall_percentiles" class="chart-container"></div> | 
|  | 424 | +                <div id="overall_percentiles" class="chart-container" data-legend-target="legend-overall_percentiles"></div> | 
|  | 425 | +                <div class="chart-legend" id="legend-overall_percentiles"></div> | 
| 361 | 426 |             </div> | 
| 362 | 427 |             <div class="chart-card"> | 
| 363 | 428 |                 <div class="chart-title">Response Time vs Time (Per Endpoint)</div> | 
| 364 |  | -                <div id="endpoint_response_time" class="chart-container"></div> | 
|  | 429 | +                <div id="endpoint_response_time" class="chart-container" data-legend-target="legend-endpoint_response_time"></div> | 
|  | 430 | +                <div class="chart-legend" id="legend-endpoint_response_time"></div> | 
| 365 | 431 |             </div> | 
| 366 | 432 |             <div class="chart-card"> | 
| 367 | 433 |                 <div class="chart-title">Requests Per Second (Per Endpoint)</div> | 
| 368 |  | -                <div id="endpoint_requests" class="chart-container"></div> | 
|  | 434 | +                <div id="endpoint_requests" class="chart-container" data-legend-target="legend-endpoint_requests"></div> | 
|  | 435 | +                <div class="chart-legend" id="legend-endpoint_requests"></div> | 
| 369 | 436 |             </div> | 
| 370 | 437 |             <div class="chart-card"> | 
| 371 | 438 |                 <div class="chart-title">Average Content Size Per Second (Per Endpoint)</div> | 
| 372 |  | -                <div id="endpoint_throughput" class="chart-container"></div> | 
|  | 439 | +                <div id="endpoint_throughput" class="chart-container" data-legend-target="legend-endpoint_throughput"></div> | 
|  | 440 | +                <div class="chart-legend" id="legend-endpoint_throughput"></div> | 
| 373 | 441 |             </div> | 
| 374 | 442 |         </div> | 
| 375 | 443 | 
 | 
| @@ -588,27 +656,104 @@ <h2>Endpoint Highlights</h2> | 
| 588 | 656 |             if (element.previousElementSibling && element.previousElementSibling.classList.contains('chart-title')) { | 
| 589 | 657 |                 element.previousElementSibling.textContent = title; | 
| 590 | 658 |             } | 
|  | 659 | +            const legendTargetId = element.dataset.legendTarget; | 
|  | 660 | +            const legendContainer = legendTargetId ? document.getElementById(legendTargetId) : null; | 
|  | 661 | +            const legendNames = Array.from(new Set(series.map(item => item.name).filter(Boolean))); | 
|  | 662 | +            const legendSelected = {}; | 
|  | 663 | +            legendNames.forEach(name => { | 
|  | 664 | +                legendSelected[name] = true; | 
|  | 665 | +            }); | 
|  | 666 | + | 
| 591 | 667 |             const chart = echarts.init(element, 'light'); | 
| 592 | 668 |             const baseOptions = { | 
| 593 | 669 |                 color: ['#7f28ff', '#f50384', '#c084fc', '#c49cff', '#e9d5ff', '#10b981', '#f59e0b'], | 
| 594 | 670 |                 tooltip: { trigger: 'axis' }, | 
| 595 |  | -                legend: { type: 'scroll' }, | 
|  | 671 | +                legend: { | 
|  | 672 | +                    show: false, | 
|  | 673 | +                    data: legendNames, | 
|  | 674 | +                    selected: legendSelected | 
|  | 675 | +                }, | 
| 596 | 676 |                 toolbox: { feature: { saveAsImage: {} } }, | 
| 597 |  | -                xAxis: { type: 'time' }, | 
|  | 677 | +                xAxis: { | 
|  | 678 | +                    type: 'time', | 
|  | 679 | +                    axisLabel: { hideOverlap: true, rotate: 25, color: '#4b3f6b', fontSize: 11 }, | 
|  | 680 | +                    axisTick: { alignWithLabel: true }, | 
|  | 681 | +                    splitLine: { show: false } | 
|  | 682 | +                }, | 
| 598 | 683 |                 yAxis: { | 
| 599 | 684 |                     type: 'value', | 
| 600 | 685 |                     name: yAxisName, | 
| 601 | 686 |                     axisLine: { symbol: 'arrow' }, | 
| 602 | 687 |                     splitLine: { show: true, lineStyle: { color: '#ede3ff' } } | 
| 603 | 688 |                 }, | 
| 604 |  | -                grid: { top: 24, left: 60, right: 30, bottom: 50 }, | 
|  | 689 | +                grid: { top: 40, left: 60, right: 30, bottom: 60 }, | 
| 605 | 690 |                 series: series | 
| 606 | 691 |             }; | 
| 607 |  | -            chart.setOption(Object.assign(baseOptions, options)); | 
|  | 692 | +            const mergedOptions = Object.assign({}, baseOptions, options); | 
|  | 693 | +            if (options.legend) { | 
|  | 694 | +                mergedOptions.legend = Object.assign({}, baseOptions.legend, options.legend); | 
|  | 695 | +            } | 
|  | 696 | +            if (options.xAxis) { | 
|  | 697 | +                mergedOptions.xAxis = Object.assign({}, baseOptions.xAxis, options.xAxis); | 
|  | 698 | +            } | 
|  | 699 | +            if (options.yAxis) { | 
|  | 700 | +                mergedOptions.yAxis = Object.assign({}, baseOptions.yAxis, options.yAxis); | 
|  | 701 | +            } | 
|  | 702 | +            if (options.grid) { | 
|  | 703 | +                mergedOptions.grid = Object.assign({}, baseOptions.grid, options.grid); | 
|  | 704 | +            } | 
|  | 705 | +            chart.setOption(mergedOptions); | 
|  | 706 | +            if (legendContainer) { | 
|  | 707 | +                renderExternalLegend(chart, legendContainer, legendNames); | 
|  | 708 | +            } | 
| 608 | 709 |             charts.push(chart); | 
| 609 | 710 |             return chart; | 
| 610 | 711 |         } | 
| 611 | 712 | 
 | 
|  | 713 | +        function renderExternalLegend(chart, container, legendNames) { | 
|  | 714 | +            if (!container) { | 
|  | 715 | +                return; | 
|  | 716 | +            } | 
|  | 717 | +            container.innerHTML = ''; | 
|  | 718 | +            const buttons = {}; | 
|  | 719 | + | 
|  | 720 | +            legendNames.forEach(name => { | 
|  | 721 | +                const button = document.createElement('button'); | 
|  | 722 | +                button.type = 'button'; | 
|  | 723 | +                button.textContent = name; | 
|  | 724 | +                button.addEventListener('click', () => { | 
|  | 725 | +                    chart.dispatchAction({ | 
|  | 726 | +                        type: 'legendToggleSelect', | 
|  | 727 | +                        name: name, | 
|  | 728 | +                    }); | 
|  | 729 | +                    updateState(); | 
|  | 730 | +                }); | 
|  | 731 | +                container.appendChild(button); | 
|  | 732 | +                buttons[name] = button; | 
|  | 733 | +            }); | 
|  | 734 | + | 
|  | 735 | +            chart.on('legendselectchanged', updateState); | 
|  | 736 | +            chart.on('restore', updateState); | 
|  | 737 | +            updateState(); | 
|  | 738 | + | 
|  | 739 | +            function updateState() { | 
|  | 740 | +                const option = chart.getOption(); | 
|  | 741 | +                const legend = Array.isArray(option.legend) ? option.legend[0] : option.legend; | 
|  | 742 | +                const selected = (legend && legend.selected) || {}; | 
|  | 743 | +                legendNames.forEach(name => { | 
|  | 744 | +                    const button = buttons[name]; | 
|  | 745 | +                    if (!button) { | 
|  | 746 | +                        return; | 
|  | 747 | +                    } | 
|  | 748 | +                    if (selected[name] === false) { | 
|  | 749 | +                        button.classList.remove('active'); | 
|  | 750 | +                    } else { | 
|  | 751 | +                        button.classList.add('active'); | 
|  | 752 | +                    } | 
|  | 753 | +                }); | 
|  | 754 | +            } | 
|  | 755 | +        } | 
|  | 756 | + | 
| 612 | 757 |         createLineChart('overall_requests', 'Requests Per Second (Total)', [{ | 
| 613 | 758 |             name: 'Requests', | 
| 614 | 759 |             type: 'line', | 
|  | 
0 commit comments