|
1 | 1 | import React from "react"; |
2 | 2 | import { Pressable, Text, View } from "react-native"; |
3 | 3 |
|
4 | | -import { CandlebarChart, ComboChart } from "@chart-kit/pro"; |
| 4 | +import { CandlebarChart, ComboChart, Realtime } from "@chart-kit/pro"; |
5 | 5 | import { |
6 | 6 | resolveCartesianChartThemeConfig, |
7 | 7 | useChartKitTheme |
@@ -37,6 +37,33 @@ const crosshairCandlebarPrices = Array.from({ length: 40 }, (_, index) => { |
37 | 37 | }; |
38 | 38 | }); |
39 | 39 |
|
| 40 | +const realtimeUpdateMs = 2000; |
| 41 | + |
| 42 | +const getRealtimeUsersAt = (pointIndex: number) => { |
| 43 | + const value = |
| 44 | + 44 + |
| 45 | + Math.sin(pointIndex * 0.7) * 16 + |
| 46 | + Math.cos(pointIndex * 0.25) * 10 + |
| 47 | + (pointIndex % 5) * 3; |
| 48 | + |
| 49 | + return Math.max(8, Math.min(92, Math.round(value))); |
| 50 | +}; |
| 51 | + |
| 52 | +const createRealtimeRows = (tick: number, count = 30) => |
| 53 | + Array.from({ length: count }, (_, index) => { |
| 54 | + const pointIndex = tick + index; |
| 55 | + |
| 56 | + return { |
| 57 | + pointIndex, |
| 58 | + users: getRealtimeUsersAt(pointIndex) |
| 59 | + }; |
| 60 | + }); |
| 61 | + |
| 62 | +const formatRealtimeAgeLabel = (minutesAgo: number) => |
| 63 | + minutesAgo === 0 ? "Now" : `${minutesAgo} min ago`; |
| 64 | + |
| 65 | +type RealtimeChartXValue = Date | number | string; |
| 66 | + |
40 | 67 | export const CandlebarCrosshairPreview = ({ |
41 | 68 | isMostMobile, |
42 | 69 | mode, |
@@ -435,6 +462,141 @@ export const CandlebarRealtimePreview = ({ |
435 | 462 | ); |
436 | 463 | }; |
437 | 464 |
|
| 465 | +export const RealtimeBarChartPreview = ({ |
| 466 | + isMostMobile, |
| 467 | + width |
| 468 | +}: { |
| 469 | + isMostMobile: boolean; |
| 470 | + width: number; |
| 471 | +}) => { |
| 472 | + const chartKitTheme = useChartKitTheme(); |
| 473 | + const chartWidth = clampChartWidth(width); |
| 474 | + const [tick, setTick] = React.useState(0); |
| 475 | + const rows = React.useMemo(() => createRealtimeRows(tick), [tick]); |
| 476 | + const total = rows.reduce((sum, row) => sum + row.users, 0); |
| 477 | + const resolvedTheme = resolveCartesianChartThemeConfig({ |
| 478 | + mode: chartKitTheme.mode, |
| 479 | + preset: chartKitTheme.preset, |
| 480 | + presets: chartKitTheme.presets, |
| 481 | + theme: chartKitTheme.theme |
| 482 | + }); |
| 483 | + const primaryColor = resolvedTheme.series[0] ?? "#2563eb"; |
| 484 | + |
| 485 | + React.useEffect(() => { |
| 486 | + const intervalId = window.setInterval(() => { |
| 487 | + setTick((currentTick) => currentTick + 1); |
| 488 | + }, realtimeUpdateMs); |
| 489 | + |
| 490 | + return () => window.clearInterval(intervalId); |
| 491 | + }, []); |
| 492 | + |
| 493 | + const formatXLabel = React.useCallback( |
| 494 | + (value: RealtimeChartXValue) => { |
| 495 | + const pointIndex = |
| 496 | + typeof value === "number" |
| 497 | + ? value |
| 498 | + : value instanceof Date |
| 499 | + ? Number.NaN |
| 500 | + : Number.parseInt(value, 10); |
| 501 | + const minutesAgo = Number.isFinite(pointIndex) |
| 502 | + ? Math.max(0, tick + 29 - pointIndex) |
| 503 | + : 0; |
| 504 | + |
| 505 | + return formatRealtimeAgeLabel(minutesAgo); |
| 506 | + }, |
| 507 | + [tick] |
| 508 | + ); |
| 509 | + |
| 510 | + return ( |
| 511 | + <View |
| 512 | + style={{ |
| 513 | + width: chartWidth, |
| 514 | + overflow: "hidden", |
| 515 | + borderColor: resolvedTheme.axis, |
| 516 | + borderRadius: 8, |
| 517 | + borderWidth: 1, |
| 518 | + backgroundColor: resolvedTheme.background, |
| 519 | + paddingBottom: isMostMobile ? 8 : 10 |
| 520 | + }} |
| 521 | + > |
| 522 | + <View |
| 523 | + style={{ |
| 524 | + width: chartWidth, |
| 525 | + borderBottomColor: resolvedTheme.grid, |
| 526 | + borderBottomWidth: 1, |
| 527 | + backgroundColor: resolvedTheme.plotBackground, |
| 528 | + paddingHorizontal: isMostMobile ? 10 : 12, |
| 529 | + paddingVertical: isMostMobile ? 8 : 10, |
| 530 | + flexDirection: "row", |
| 531 | + alignItems: "center", |
| 532 | + justifyContent: "space-between", |
| 533 | + gap: 12 |
| 534 | + }} |
| 535 | + > |
| 536 | + <View style={{ minWidth: 0 }}> |
| 537 | + <Text |
| 538 | + style={{ |
| 539 | + color: resolvedTheme.text, |
| 540 | + fontSize: isMostMobile ? 13 : 14, |
| 541 | + fontWeight: "800" |
| 542 | + }} |
| 543 | + > |
| 544 | + Active users |
| 545 | + </Text> |
| 546 | + <Text |
| 547 | + style={{ |
| 548 | + color: resolvedTheme.mutedText, |
| 549 | + fontSize: 10, |
| 550 | + fontWeight: "700", |
| 551 | + marginTop: 2 |
| 552 | + }} |
| 553 | + > |
| 554 | + Last 30 minutes |
| 555 | + </Text> |
| 556 | + </View> |
| 557 | + <Text |
| 558 | + style={{ |
| 559 | + color: primaryColor, |
| 560 | + fontSize: isMostMobile ? 13 : 14, |
| 561 | + fontVariant: ["tabular-nums"], |
| 562 | + fontWeight: "800" |
| 563 | + }} |
| 564 | + > |
| 565 | + {total.toLocaleString("en-US")} |
| 566 | + </Text> |
| 567 | + </View> |
| 568 | + <Realtime.BarChart |
| 569 | + accessibilityLabel="Active users per minute over a rolling thirty minute window" |
| 570 | + animation={{ duration: realtimeUpdateMs, mode: "slide" }} |
| 571 | + barRadius={3} |
| 572 | + barWidthRatio={0.82} |
| 573 | + data={rows} |
| 574 | + defaultSelectedBar={{ dataIndex: rows.length - 1, seriesKey: "users" }} |
| 575 | + formatXLabel={formatXLabel} |
| 576 | + formatYLabel={(value: number) => String(Math.round(value))} |
| 577 | + height={150} |
| 578 | + interaction="tap" |
| 579 | + labelStrategy="hide" |
| 580 | + liveKey="pointIndex" |
| 581 | + series={[{ yKey: "users", label: "Users", color: primaryColor }]} |
| 582 | + showHorizontalGridLines |
| 583 | + showXAxisLabels={false} |
| 584 | + showYAxisLabels={false} |
| 585 | + tooltip={{ |
| 586 | + anchor: "bar", |
| 587 | + placement: "top", |
| 588 | + width: 108 |
| 589 | + }} |
| 590 | + width={chartWidth} |
| 591 | + windowSize={30} |
| 592 | + xKey="pointIndex" |
| 593 | + yDomain={[0, 100]} |
| 594 | + yKey="users" |
| 595 | + /> |
| 596 | + </View> |
| 597 | + ); |
| 598 | +}; |
| 599 | + |
438 | 600 | const getComboToggleItems = (mode: "dark" | "light") => { |
439 | 601 | const series = |
440 | 602 | mode === "dark" |
|
0 commit comments