|
487 | 487 | } |
488 | 488 |
|
489 | 489 | // Colored stacked velocity: languages tab |
490 | | - // Note: 'prod-vs-test' sub-mode is treated as 'languages-only' — no prod/test velocity split |
491 | 490 | if (tab === 'languages') { |
| 491 | + if (langSubMode === 'prod-vs-test') { |
| 492 | + // Two stacks: Production velocity and Test velocity (derived from cumulative data) |
| 493 | + const prodDeltas = daysList.map((d, idx) => { |
| 494 | + let prod = 0 |
| 495 | + for (const lc of Object.values(d.languages)) prod += lc.prod ?? lc.total |
| 496 | + if (idx === 0) return prod |
| 497 | + let prevProd = 0 |
| 498 | + for (const lc of Object.values(daysList[idx - 1].languages)) prevProd += lc.prod ?? lc.total |
| 499 | + return Math.abs(prod - prevProd) |
| 500 | + }) |
| 501 | + const testDeltas = daysList.map((d, idx) => { |
| 502 | + let test = 0 |
| 503 | + for (const lc of Object.values(d.languages)) test += lc.test ?? 0 |
| 504 | + if (idx === 0) return test |
| 505 | + let prevTest = 0 |
| 506 | + for (const lc of Object.values(daysList[idx - 1].languages)) prevTest += lc.test ?? 0 |
| 507 | + return Math.abs(test - prevTest) |
| 508 | + }) |
| 509 | + return [ |
| 510 | + { |
| 511 | + label: 'Production', |
| 512 | + data: prodDeltas, |
| 513 | + backgroundColor: getChartColor(0) + '80', |
| 514 | + borderColor: getChartColor(0), |
| 515 | + borderWidth: 1.5, |
| 516 | + fill: 'origin', |
| 517 | + tension: 0.3, |
| 518 | + pointRadius: 0, |
| 519 | + pointHitRadius: 6, |
| 520 | + }, |
| 521 | + { |
| 522 | + label: 'Test', |
| 523 | + data: testDeltas, |
| 524 | + backgroundColor: getChartTint(0) + '80', |
| 525 | + borderColor: getChartTint(0), |
| 526 | + borderWidth: 1.5, |
| 527 | + fill: '-1', |
| 528 | + tension: 0.3, |
| 529 | + pointRadius: 0, |
| 530 | + pointHitRadius: 6, |
| 531 | + }, |
| 532 | + ] |
| 533 | + } |
| 534 | +
|
| 535 | + // 'all' and 'languages-only' modes |
492 | 536 | const { shown, other: hasOther } = computeVisibleLanguages(daysList, detectedLanguages) |
493 | 537 | const datasets: ChartDataset<'line'>[] = [] |
494 | 538 |
|
495 | 539 | for (let i = 0; i < shown.length; i++) { |
496 | 540 | const langId = shown[i] |
497 | 541 | const colorIdx = i % maxChartColors |
498 | | - const rawActivity = daysList.map((d) => { |
499 | | - const added = d.languageAdded?.[langId] ?? 0 |
500 | | - const removed = d.languageRemoved?.[langId] ?? 0 |
501 | | - return Math.max(added, removed) |
502 | | - }) |
503 | | - datasets.push({ |
504 | | - label: langName(langId), |
505 | | - data: rawActivity, |
506 | | - backgroundColor: getChartColor(colorIdx) + '80', |
507 | | - borderColor: getChartColor(colorIdx), |
508 | | - borderWidth: 1.5, |
509 | | - fill: datasets.length === 0 ? 'origin' : '-1', |
510 | | - tension: 0.3, |
511 | | - pointRadius: 0, |
512 | | - pointHitRadius: 6, |
513 | | - }) |
| 542 | + const splitTest = langSubMode === 'all' && shouldSplitTestLayer(daysList, langId) |
| 543 | +
|
| 544 | + if (splitTest) { |
| 545 | + // Prod velocity derived from cumulative prod data |
| 546 | + const prodDeltas = daysList.map((d, idx) => { |
| 547 | + const lc = d.languages[langId] |
| 548 | + const prod = lc?.prod ?? lc?.total ?? 0 |
| 549 | + if (idx === 0) return prod |
| 550 | + const prevLc = daysList[idx - 1].languages[langId] |
| 551 | + const prevProd = prevLc?.prod ?? prevLc?.total ?? 0 |
| 552 | + return Math.abs(prod - prevProd) |
| 553 | + }) |
| 554 | + datasets.push({ |
| 555 | + label: `${langName(langId)} (prod)`, |
| 556 | + data: prodDeltas, |
| 557 | + backgroundColor: getChartColor(colorIdx) + '80', |
| 558 | + borderColor: getChartColor(colorIdx), |
| 559 | + borderWidth: 1.5, |
| 560 | + fill: datasets.length === 0 ? 'origin' : '-1', |
| 561 | + tension: 0.3, |
| 562 | + pointRadius: 0, |
| 563 | + pointHitRadius: 6, |
| 564 | + }) |
| 565 | + // Test velocity derived from cumulative test data |
| 566 | + const testDeltas = daysList.map((d, idx) => { |
| 567 | + const test = d.languages[langId]?.test ?? 0 |
| 568 | + if (idx === 0) return test |
| 569 | + const prevTest = daysList[idx - 1].languages[langId]?.test ?? 0 |
| 570 | + return Math.abs(test - prevTest) |
| 571 | + }) |
| 572 | + datasets.push({ |
| 573 | + label: `${langName(langId)} (test)`, |
| 574 | + data: testDeltas, |
| 575 | + backgroundColor: getChartTint(colorIdx) + '80', |
| 576 | + borderColor: getChartTint(colorIdx), |
| 577 | + borderWidth: 1.5, |
| 578 | + fill: datasets.length === 0 ? 'origin' : '-1', |
| 579 | + tension: 0.3, |
| 580 | + pointRadius: 0, |
| 581 | + pointHitRadius: 6, |
| 582 | + }) |
| 583 | + } else { |
| 584 | + const rawActivity = daysList.map((d) => { |
| 585 | + const added = d.languageAdded?.[langId] ?? 0 |
| 586 | + const removed = d.languageRemoved?.[langId] ?? 0 |
| 587 | + return Math.max(added, removed) |
| 588 | + }) |
| 589 | + datasets.push({ |
| 590 | + label: langName(langId), |
| 591 | + data: rawActivity, |
| 592 | + backgroundColor: getChartColor(colorIdx) + '80', |
| 593 | + borderColor: getChartColor(colorIdx), |
| 594 | + borderWidth: 1.5, |
| 595 | + fill: datasets.length === 0 ? 'origin' : '-1', |
| 596 | + tension: 0.3, |
| 597 | + pointRadius: 0, |
| 598 | + pointHitRadius: 6, |
| 599 | + }) |
| 600 | + } |
514 | 601 | } |
515 | 602 |
|
516 | 603 | if (hasOther) { |
|
933 | 1020 | <button |
934 | 1021 | onclick={() => (languageSubMode = 'all')} |
935 | 1022 | aria-pressed={languageSubMode === 'all'} |
936 | | - disabled={velocityEnabled} |
937 | 1023 | class="strata-chip">All</button |
938 | 1024 | > |
939 | 1025 | <button |
940 | 1026 | onclick={() => (languageSubMode = 'prod-vs-test')} |
941 | 1027 | aria-pressed={languageSubMode === 'prod-vs-test'} |
942 | | - disabled={velocityEnabled} |
943 | 1028 | class="strata-chip">Prod vs test</button |
944 | 1029 | > |
945 | 1030 | <button |
946 | 1031 | onclick={() => (languageSubMode = 'languages-only')} |
947 | 1032 | aria-pressed={languageSubMode === 'languages-only'} |
948 | | - disabled={velocityEnabled} |
949 | 1033 | class="strata-chip">Languages only</button |
950 | 1034 | > |
951 | 1035 | {:else} |
|
0 commit comments