diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md
index 2c6a453..9bc9fe1 100644
--- a/docs/EMBEDDING.md
+++ b/docs/EMBEDDING.md
@@ -31,6 +31,8 @@ The component requires its bundled CSS. Import it once at your application root:
import '@ioai/rosview/style.css';
```
+Styles are scoped to `#rosview-root` inside the bundle, so Tailwind preflight does not reset global elements (navbar, buttons, etc.) in your host application.
+
---
## Step 2 — Basic usage
@@ -133,6 +135,29 @@ const rows = parseRemoteDatasetListJson(await res.json());
---
+## Advanced: Navbar branding
+
+The navbar shows **ROS View** on the left by default. When embedding inside a larger app, you can hide it or replace it with your product name:
+
+```tsx
+// Hide the brand button (File / Layout menus remain)
+
+
+// Replace with host branding
+
+```
+
+Related props:
+
+| Prop | Description |
+|------|-------------|
+| `showNavbarBrand` | Left brand button visibility (`true` by default). |
+| `navbarBrandLabel` | Custom brand text; defaults to the localized product name. |
+| `navbarSourceName` | Center label for the active dataset (separate from brand). |
+| `showNavbar` | Hides the entire navbar when `false` (via `chrome` or explicit prop). |
+
+---
+
## Advanced: Controlled theme & language
Disable internal localStorage persistence and fully control state from your application:
diff --git a/docs/EMBEDDING.zh.md b/docs/EMBEDDING.zh.md
index 2f49bab..84b5295 100644
--- a/docs/EMBEDDING.zh.md
+++ b/docs/EMBEDDING.zh.md
@@ -31,6 +31,8 @@ npm install @ioai/rosview
import '@ioai/rosview/style.css';
```
+样式在构建产物中限定在 `#rosview-root` 内,Tailwind preflight 不会重置宿主应用的全局元素(导航栏、按钮等)。
+
---
## 步骤 2 — 基本用法
@@ -133,6 +135,29 @@ const rows = parseRemoteDatasetListJson(await res.json());
---
+## 进阶:Navbar 品牌文案
+
+Navbar 左侧默认显示 **ROS View**。嵌入到更大页面时,可以隐藏或替换为你的产品名:
+
+```tsx
+// 隐藏品牌按钮(File / Layout 菜单仍保留)
+
+
+// 替换为宿主品牌
+
+```
+
+相关 props:
+
+| Prop | 说明 |
+|------|------|
+| `showNavbarBrand` | 左侧品牌按钮是否显示(默认 `true`)。 |
+| `navbarBrandLabel` | 自定义品牌文案;未设置时使用本地化产品名。 |
+| `navbarSourceName` | 中间区域的数据源名称(与品牌文案无关)。 |
+| `showNavbar` | 设为 `false` 时隐藏整条 Navbar(通过 `chrome` 或显式 prop)。 |
+
+---
+
## 进阶:受控主题与语言
关闭组件内部的 localStorage 持久化,由宿主应用完全控制状态:
diff --git a/package-lock.json b/package-lock.json
index 96536ff..284dbff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@ioai/rosview",
- "version": "1.5.1",
+ "version": "1.5.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ioai/rosview",
- "version": "1.5.1",
+ "version": "1.5.2",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.39.4",
diff --git a/package.json b/package.json
index f794939..9feb39c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@ioai/rosview",
- "version": "1.5.1",
+ "version": "1.5.2",
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
"keywords": [
"ros",
diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx
index 1130b38..3065814 100644
--- a/src/app/AppShell.tsx
+++ b/src/app/AppShell.tsx
@@ -27,6 +27,8 @@ interface AppShellProps {
onLanguageChange: (lang: 'en' | 'zh' | 'ja') => void;
showLanguageSwitcher?: boolean;
showThemeSwitcher?: boolean;
+ showNavbarBrand?: boolean;
+ navbarBrandLabel?: string;
onBrandClick?: () => void;
preferAutoLayout?: boolean;
preferencePersistence: PreferencePersistence;
@@ -77,6 +79,8 @@ export const AppShell: React.FC = ({
onLanguageChange,
showLanguageSwitcher = true,
showThemeSwitcher = true,
+ showNavbarBrand = true,
+ navbarBrandLabel,
onBrandClick,
preferAutoLayout = false,
preferencePersistence,
@@ -150,6 +154,8 @@ export const AppShell: React.FC = ({
onLanguageChange={onLanguageChange}
showLanguageSwitcher={showLanguageSwitcher}
showThemeSwitcher={showThemeSwitcher}
+ showNavbarBrand={showNavbarBrand}
+ brandLabel={navbarBrandLabel}
onBrandClick={onBrandClick}
onOpenFilePick={hideOpenFileMenus ? undefined : onOpenFilePick}
onOpenDirectory={hideOpenFileMenus ? undefined : onOpenDirectory}
diff --git a/src/features/layout/DockviewLayout.tsx b/src/features/layout/DockviewLayout.tsx
index ad5f7bb..5389ed3 100644
--- a/src/features/layout/DockviewLayout.tsx
+++ b/src/features/layout/DockviewLayout.tsx
@@ -381,6 +381,7 @@ export const DockviewLayout: React.FC = ({
if (!apiRef.current || topics.length === 0) return;
const api = apiRef.current;
if (
+ preferAutoLayoutRef.current &&
hasAutoInitializedRef.current &&
layoutDatasetSignatureRef.current &&
layoutDatasetSignatureRef.current !== currentDatasetSignature
@@ -389,6 +390,14 @@ export const DockviewLayout: React.FC = ({
layoutDatasetSignatureRef.current = currentDatasetSignature;
return;
}
+ if (
+ hasAutoInitializedRef.current &&
+ layoutDatasetSignatureRef.current &&
+ layoutDatasetSignatureRef.current !== currentDatasetSignature
+ ) {
+ layoutDatasetSignatureRef.current = currentDatasetSignature;
+ return;
+ }
if (hasAutoInitializedRef.current) return;
const hasAnyNonWelcomePanel = api.panels.some((panel) => panel.id !== WELCOME_PANEL_ID);
if (hasAnyNonWelcomePanel) return;
diff --git a/src/features/panels/Plot/usePlotChart.test.ts b/src/features/panels/Plot/usePlotChart.test.ts
index 382e21e..b5dcb4d 100644
--- a/src/features/panels/Plot/usePlotChart.test.ts
+++ b/src/features/panels/Plot/usePlotChart.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
diffSeriesTopology,
+ shouldPinPlotXScaleToLogRange,
shouldRemountForIncrementalSeriesUpdate,
type SeriesSignature,
} from './usePlotChart';
@@ -86,3 +87,19 @@ describe('shouldRemountForIncrementalSeriesUpdate', () => {
.toBe(false);
});
});
+
+describe('shouldPinPlotXScaleToLogRange', () => {
+ const logRange = { min: 0, max: 55 };
+
+ it('pins when log range exists and following view is off', () => {
+ expect(shouldPinPlotXScaleToLogRange(logRange, 0)).toBe(true);
+ });
+
+ it('does not pin without log range', () => {
+ expect(shouldPinPlotXScaleToLogRange(undefined, 0)).toBe(false);
+ });
+
+ it('does not pin when playhead following owns the X axis', () => {
+ expect(shouldPinPlotXScaleToLogRange(logRange, 10)).toBe(false);
+ });
+});
diff --git a/src/features/panels/Plot/usePlotChart.ts b/src/features/panels/Plot/usePlotChart.ts
index bb475c5..1698c7b 100644
--- a/src/features/panels/Plot/usePlotChart.ts
+++ b/src/features/panels/Plot/usePlotChart.ts
@@ -171,6 +171,25 @@ export function shouldRemountForIncrementalSeriesUpdate(
return false;
}
+/**
+ * Whether incremental updates should pin the X axis to the full log range.
+ * Skipped when playhead-following mode owns the X scale.
+ */
+export function shouldPinPlotXScaleToLogRange(
+ xRange: { min: number; max: number } | undefined,
+ followingViewWidthSec: number,
+): xRange is { min: number; max: number } {
+ return xRange != null && followingViewWidthSec <= 0;
+}
+
+/** Keep the X viewport on the full recording duration during range reads. */
+export function pinPlotXScaleToLogRange(
+ chart: uPlot,
+ xRange: { min: number; max: number },
+): void {
+ chart.setScale('x', xRange);
+}
+
export function usePlotChart({
containerRef,
player,
@@ -228,8 +247,15 @@ export function usePlotChart({
isLoading: () => loadingRef.current,
});
- uplotRef.current = mountPlotChart(container, dataset, options, xRange);
+ const chart = mountPlotChart(container, dataset, options, xRange);
+ uplotRef.current = chart;
seriesSignaturesRef.current = seriesSignatures(dataset, hiddenSeries);
+ if (
+ loadingRef.current
+ && shouldPinPlotXScaleToLogRange(xRange, followingViewWidthRef.current)
+ ) {
+ pinPlotXScaleToLogRange(chart, xRange);
+ }
const observer = new ResizeObserver(() => {
const chart = uplotRef.current;
@@ -309,13 +335,32 @@ export function usePlotChart({
// setData second arg = false avoids a hard scale reset every batch, which is
// what made the chart flash while data was streaming in.
chart.setData(dataset.data, false);
+ // setData(false) can shrink X to the loaded points only; pin to the full log
+ // range so the axis stays 0…duration while curves grow incrementally.
+ if (
+ loadingRef.current
+ && shouldPinPlotXScaleToLogRange(xRange, followingViewWidthRef.current)
+ ) {
+ pinPlotXScaleToLogRange(chart, xRange);
+ }
// Style mutations are read at draw time, so force one rebuild+redraw to
// surface the new width/dash/stroke immediately.
if (diff.kind === 'styleUpdate') {
chart.redraw(true);
+ } else if (loadingRef.current) {
+ chart.redraw(false);
}
seriesSignaturesRef.current = nextSignatures;
- }, [config.followingViewWidthSec, config.xAxisMode, containerRef, dataset, destroyChart, hiddenSeries, mountChart]);
+ }, [
+ config.followingViewWidthSec,
+ config.xAxisMode,
+ containerRef,
+ dataset,
+ destroyChart,
+ hiddenSeries,
+ mountChart,
+ xRange,
+ ]);
// When loading completes, force one Y-scale recompute so the locked-min/max
// is replaced with the natural auto-fit range.
diff --git a/src/features/viewer/RosViewerImpl.tsx b/src/features/viewer/RosViewerImpl.tsx
index febd99f..f075b67 100644
--- a/src/features/viewer/RosViewerImpl.tsx
+++ b/src/features/viewer/RosViewerImpl.tsx
@@ -250,6 +250,10 @@ export interface RosViewerProps {
extensions?: RosViewExtension[];
/** Optional center label override shown in navbar source area. */
navbarSourceName?: string;
+ /** Whether to show the left navbar brand button. @default true */
+ showNavbarBrand?: boolean;
+ /** Custom label for the left navbar brand button (defaults to product name). */
+ navbarBrandLabel?: string;
/** Whether to show navbar language switcher. @default true */
showLanguageSwitcher?: boolean;
/** Whether to show navbar theme switcher. @default true */
@@ -1145,6 +1149,8 @@ export const RosViewer: React.FC = (props) => {
onLanguageChange={handleLanguageChange}
showLanguageSwitcher={props.showLanguageSwitcher ?? true}
showThemeSwitcher={props.showThemeSwitcher ?? true}
+ showNavbarBrand={props.showNavbarBrand ?? true}
+ navbarBrandLabel={props.navbarBrandLabel}
onBrandClick={handleGoHome}
preferAutoLayout={props.preferAutoLayout ?? false}
preferencePersistence={persistence}
@@ -1206,6 +1212,8 @@ export const RosViewer: React.FC = (props) => {
onLanguageChange={handleLanguageChange}
showLanguageSwitcher={props.showLanguageSwitcher ?? true}
showThemeSwitcher={props.showThemeSwitcher ?? true}
+ showNavbarBrand={props.showNavbarBrand ?? true}
+ brandLabel={props.navbarBrandLabel}
onBrandClick={handleGoHome}
onOpenFilePick={() => {
clearOpenFeedback();
@@ -1309,6 +1317,8 @@ export const RosViewer: React.FC = (props) => {
onLanguageChange={handleLanguageChange}
showLanguageSwitcher={props.showLanguageSwitcher ?? true}
showThemeSwitcher={props.showThemeSwitcher ?? true}
+ showNavbarBrand={props.showNavbarBrand ?? true}
+ brandLabel={props.navbarBrandLabel}
onBrandClick={handleGoHome}
onOpenFilePick={() => {
clearOpenFeedback();
diff --git a/src/features/workspace/navbar/Navbar.tsx b/src/features/workspace/navbar/Navbar.tsx
index 1ff87c6..d491612 100644
--- a/src/features/workspace/navbar/Navbar.tsx
+++ b/src/features/workspace/navbar/Navbar.tsx
@@ -83,6 +83,10 @@ interface NavbarProps {
onLanguageChange?: (lang: 'en' | 'zh' | 'ja') => void;
showLanguageSwitcher?: boolean;
showThemeSwitcher?: boolean;
+ /** When false, hide the left brand button. @default true */
+ showNavbarBrand?: boolean;
+ /** Override default product name in the left brand button. */
+ brandLabel?: string;
onBrandClick?: () => void;
onOpenFilePick?: () => void;
onOpenDirectory?: () => void;
@@ -103,6 +107,8 @@ export const Navbar: React.FC = ({
onLanguageChange,
showLanguageSwitcher = true,
showThemeSwitcher = true,
+ showNavbarBrand = true,
+ brandLabel,
onBrandClick,
onOpenFilePick,
onOpenDirectory,
@@ -164,20 +170,24 @@ export const Navbar: React.FC = ({
onOpenFilePick || onOpenDirectory || onOpenTarPick || onOpenRemotePrompt || onOpenSampleDialog;
const showCenter = Boolean(sourceLoading || (sourceName && sourceName.trim().length > 0));
const centerLabel = sourceLoading ? formatMessage({ id: 'navbar.sourceLoading' }) : (sourceName ?? '');
+ const brandText = brandLabel ?? formatMessage({ id: 'common.productName' });
+ const brandAccessibleName = brandLabel ?? formatMessage({ id: 'navbar.goHome' });
return (