데이터 매니저 원본

SpreadJS 피벗 테이블은 데이터를 워크시트 범위로 먼저 구체화하지 않고 데이터 매니저 테이블이나 뷰를 직접 분석할 수 있습니다. 데이터 매니저 데이터가 변경되면 피벗 테이블을 자동으로 새로 고침하여 최신 결과를 반영할 수 있습니다.

데이터 매니저 원본 SpreadJS 피벗 테이블은 데이터를 워크시트 범위나 워크시트 테이블로 먼저 구체화하지 않고 데이터 매니저 테이블 또는 데이터 매니저 뷰를 데이터 원본으로 직접 사용할 수 있습니다. source 옵션으로 데이터 매니저 개체를 지정합니다. source: "tableName"은 데이터 매니저 테이블을 직접 사용합니다. source: "tableName.viewName"은 지정된 테이블 아래의 데이터 매니저 뷰를 사용합니다. 데이터 매니저 테이블에서 피벗 테이블을 만들 수 있습니다. 데이터 매니저 뷰에서도 피벗 테이블을 만들 수 있습니다. 뷰는 관련 테이블의 필드를 노출할 수 있으므로 하나의 뷰 원본을 통해 관계형 데이터 매니저 모델을 분석할 수 있습니다. 데이터 매니저 원본 데이터가 변경될 때 피벗 테이블 캐시를 자동으로 새로 고칠지 여부는 autoRefresh로 제어합니다. autoRefresh가 true이면 피벗 테이블이 데이터 매니저 원본 변경 내용을 자동으로 반영합니다. false이면 데이터 매니저 데이터가 변경되어도 피벗 테이블은 수동으로 새로 고칠 때까지 현재 캐시를 유지합니다. 현재 데이터 매니저 원본에서 피벗 테이블을 새로 고치려면 인수 없이 updateSource()를 호출합니다. 원본을 변경하거나 autoRefresh 같은 원본 옵션을 업데이트하려면 데이터 매니저 원본 개체를 전달합니다.
var spread; var tableSalesTable; var viewSalesTable; var productsTable; var salesMenTable; var salesRegionsTable; var salesAnalysisView; var tablePivotTable; var viewPivotTable; var pivotPanel; var timerId; var isPivotPanelVisible = false; var sourceStates = { DataTable: { recordCount: 0, pivotRefreshTime: "", autoRefresh: true }, DataView: { recordCount: 0, pivotRefreshTime: "", autoRefresh: true } }; var salesManProductMap = {}; var people = ["Alan", "Bob", "John"]; var cars = [ { name: "Mercedes", price: 2700 }, { name: "Focus", price: 500 }, { name: "BMW", price: 1800 }, { name: "Audi", price: 1100 }, { name: "Renault", price: 800 } ]; var productsData = [ { id: 1, name: "Laptop", category: "Electronics", brand: "Northwind", price: 1299 }, { id: 2, name: "Phone", category: "Electronics", brand: "Contoso", price: 899 }, { id: 3, name: "Desk", category: "Furniture", brand: "Fabrikam", price: 420 }, { id: 4, name: "Chair", category: "Furniture", brand: "Fabrikam", price: 180 }, { id: 5, name: "Monitor", category: "Electronics", brand: "Northwind", price: 320 } ]; var salesMenData = [ { id: 1, name: "Alan", title: "Account Executive", team: "Enterprise" }, { id: 2, name: "Bob", title: "Sales Specialist", team: "Retail" }, { id: 3, name: "John", title: "Senior Sales Manager", team: "Enterprise" }, { id: 4, name: "Kevin", title: "Channel Manager", team: "Partner" }, { id: 5, name: "Michael", title: "Sales Specialist", team: "Retail" } ]; var salesRegionsData = [ { id: 1, province: "Beijing", area: "North", manager: "Grace" }, { id: 2, province: "Shanghai", area: "East", manager: "Helen" }, { id: 3, province: "Guangdong", area: "South", manager: "Ivy" }, { id: 4, province: "Zhejiang", area: "East", manager: "Helen" }, { id: 5, province: "Jiangsu", area: "East", manager: "Helen" }, { id: 6, province: "Sichuan", area: "West", manager: "Jack" }, { id: 7, province: "Hubei", area: "Central", manager: "Lily" }, { id: 8, province: "Shandong", area: "North", manager: "Grace" }, { id: 9, province: "Fujian", area: "South", manager: "Ivy" }, { id: 10, province: "Henan", area: "Central", manager: "Lily" }, { id: 11, province: "Hunan", area: "Central", manager: "Lily" }, { id: 12, province: "Shaanxi", area: "West", manager: "Jack" } ]; window.onload = function () { spread = new GC.Spread.Sheets.Workbook(document.getElementById("ss"), { sheetCount: 2 }); initSpread().then(bindEvents); }; function initSpread() { return initDataManagerSources().then(function () { spread.suspendPaint(); initDataTablePivotSheet(); initDataViewPivotSheet(); initPivotPanel(); spread.setActiveSheetIndex(0); markAllPivotRefreshed(); spread.resumePaint(); updateDemoPanel(); }); } function initDataManagerSources() { return Promise.all([ initDataTableSource(), initDataViewSource() ]); } function initDataTableSource() { var sourceData = createInitialTableSourceData(); sourceStates.DataTable.recordCount = sourceData.length; tableSalesTable = spread.dataManager().addTable("TableSales", { data: sourceData, schema: { columns: { date: { dataType: "date" }, salesperson: { dataType: "string" }, car: { dataType: "string" }, quantity: { dataType: "number" }, price: { dataType: "number" }, total: { dataType: "number" } } } }); return tableSalesTable.fetch(); } function initDataViewSource() { var dataManager = spread.dataManager(); salesManProductMap = createSalesManProductMap(); var sourceData = createInitialViewSourceData(); sourceStates.DataView.recordCount = sourceData.length; productsTable = dataManager.addTable("Products", { data: productsData, schema: { columns: { id: { dataType: "number", isPrimaryKey: true }, name: { dataType: "string" }, category: { dataType: "string" }, brand: { dataType: "string" }, price: { dataType: "number" } } } }); productsTable.primaryKey("id"); salesMenTable = dataManager.addTable("SalesMen", { data: salesMenData, schema: { columns: { id: { dataType: "number", isPrimaryKey: true }, name: { dataType: "string" }, title: { dataType: "string" }, team: { dataType: "string" } } } }); salesMenTable.primaryKey("id"); salesRegionsTable = dataManager.addTable("SalesRegions", { data: salesRegionsData, schema: { columns: { id: { dataType: "number", isPrimaryKey: true }, province: { dataType: "string" }, area: { dataType: "string" }, manager: { dataType: "string" } } } }); salesRegionsTable.primaryKey("id"); viewSalesTable = dataManager.addTable("Sales", { data: sourceData, schema: { columns: { date: { dataType: "date" }, productId: { dataType: "number" }, salesManId: { dataType: "number" }, regionId: { dataType: "number" }, quantity: { dataType: "number" }, price: { dataType: "number" }, salesTotal: { dataType: "formula", value: "=[@quantity] * [@price]" }, costTotal: { dataType: "formula", value: "=[@quantity] * [@product.price]" } } } }); dataManager.addRelationship(viewSalesTable, "productId", "product", productsTable, "id", "sales"); dataManager.addRelationship(viewSalesTable, "salesManId", "salesMan", salesMenTable, "id", "sales"); dataManager.addRelationship(viewSalesTable, "regionId", "salesRegion", salesRegionsTable, "id", "sales"); salesAnalysisView = viewSalesTable.addView("SalesAnalysisView", [ "date", // "productId", // "salesManId", // "regionId", "quantity", "price", "salesTotal", "costTotal", // "product.id", "product.name", "product.category", "product.brand", "product.price", // "salesMan.id", "salesMan.name", "salesMan.title", "salesMan.team", // "salesRegion.id", "salesRegion.province", "salesRegion.area", "salesRegion.manager" ]); return Promise.all([ productsTable.fetch(), salesMenTable.fetch(), salesRegionsTable.fetch(), viewSalesTable.fetch(), salesAnalysisView.fetch() ]); } function initDataTablePivotSheet() { var sheet = spread.getSheet(0); sheet.name("DataTable 피벗 소스"); sheet.setRowCount(1000); tablePivotTable = sheet.pivotTables.add( "dataTablePivot", getDataTablePivotSource(), 1, 1, GC.Spread.Pivot.PivotTableLayoutType.outline, GC.Spread.Pivot.PivotTableThemes.medium8, { showRowHeader: true, showColumnHeader: true, bandRows: true, bandColumns: true } ); applyDataTablePivotLayout(); } function initDataViewPivotSheet() { var sheet = spread.getSheet(1); sheet.name("DataView 피벗 소스"); sheet.setRowCount(1000); sheet.setColumnCount(30); viewPivotTable = sheet.pivotTables.add( "dataViewPivot", getDataViewPivotSource(), 1, 1, GC.Spread.Pivot.PivotTableLayoutType.outline, GC.Spread.Pivot.PivotTableThemes.medium8, { showRowHeader: true, showColumnHeader: true, bandRows: true, bandColumns: true } ); applyDataViewPivotLayout(); } function initPivotPanel() { pivotPanel = new GC.Spread.Pivot.PivotPanel("dmPivotPanel", tablePivotTable, document.getElementById("pivotPanel")); pivotPanel.sectionVisibility(GC.Spread.Pivot.PivotPanelSection.fields + GC.Spread.Pivot.PivotPanelSection.area); } function applyDataTablePivotLayout() { tablePivotTable.suspendLayout(); tablePivotTable.add("salesperson", "Salesperson", GC.Spread.Pivot.PivotTableFieldType.rowField); tablePivotTable.add("car", "Cars", GC.Spread.Pivot.PivotTableFieldType.rowField); var groupInfo = { originFieldName: "date", dateGroups: [{ by: GC.Pivot.DateGroupType.quarters }] }; tablePivotTable.group(groupInfo); tablePivotTable.add("Quarters (date)", "Quarters (date)", GC.Spread.Pivot.PivotTableFieldType.columnField); tablePivotTable.add("price", "Average Price", GC.Spread.Pivot.PivotTableFieldType.valueField, GC.Pivot.SubtotalType.average, 0); tablePivotTable.add("total", "Totals", GC.Spread.Pivot.PivotTableFieldType.valueField, GC.Pivot.SubtotalType.sum, 1); tablePivotTable.resumeLayout(); setAveragePriceFieldFormat(); tablePivotTable.autoFitColumn(); setPivotValueColumnWidth(spread.getSheet(0)); } function applyDataViewPivotLayout() { viewPivotTable.suspendLayout(); viewPivotTable.add("salesMan.name", "salesMan.name", GC.Spread.Pivot.PivotTableFieldType.rowField); viewPivotTable.add("product.name", "product.name", GC.Spread.Pivot.PivotTableFieldType.rowField); viewPivotTable.add("salesRegion.province", "salesRegion.province", GC.Spread.Pivot.PivotTableFieldType.columnField); viewPivotTable.add("salesTotal", "salesTotal", GC.Spread.Pivot.PivotTableFieldType.valueField, GC.Pivot.SubtotalType.sum); viewPivotTable.resumeLayout(); setDataViewValueFieldFormat(); viewPivotTable.autoFitColumn(); setPivotValueColumnWidth(spread.getSheet(1)); } function setAveragePriceFieldFormat() { setPivotValueFieldFormat(tablePivotTable, "Average Price"); } function setDataViewValueFieldFormat() { setPivotValueFieldFormat(viewPivotTable, "salesTotal"); setPivotValueFieldFormat(viewPivotTable, "Sum of salesTotal"); setPivotValueFieldFormat(viewPivotTable, "costTotal"); setPivotValueFieldFormat(viewPivotTable, "Sum of costTotal"); } function setPivotValueFieldFormat(pivotTable, fieldName) { var style = new GC.Spread.Sheets.Style(); style.formatter = "#,##0.00"; pivotTable.setStyle({ dataOnly: true, references: [ { fieldName: "Values", items: [fieldName] } ] }, style); } function setPivotValueColumnWidth(sheet) { for (var col = 3; col <= 14; col++) { sheet.setColumnWidth(col, 92); } } function bindEvents() { bindWorkbookEvents(); document.getElementById("autoRefresh").addEventListener("change", function () { var state = getActiveState(); state.autoRefresh = this.checked; getActivePivotTable().updateSource(getActivePivotSource()); updateDemoPanel(); }); document.getElementById("startFeed").addEventListener("click", startFeed); document.getElementById("pauseFeed").addEventListener("click", pauseFeed); document.getElementById("manualRefresh").addEventListener("click", function () { spread.suspendPaint(); getActivePivotTable().updateSource(); spread.resumePaint(); updateDemoPanel(true); }); document.getElementById("resetData").addEventListener("click", resetData); document.getElementById("togglePivotPanel").addEventListener("click", togglePivotPanel); } function bindWorkbookEvents() { spread.bind(GC.Spread.Sheets.Events.ActiveSheetChanged, function () { pauseFeed(); attachPivotPanelToActiveSheet(); updateDemoPanel(); }); } function attachPivotPanelToActiveSheet() { var pivotTable = getActivePivotTable(); if (pivotPanel && pivotTable) { pivotPanel.attach(pivotTable); } } function togglePivotPanel() { isPivotPanelVisible = !isPivotPanelVisible; updatePivotPanelVisible(); } function updatePivotPanelVisible() { var panelContainer = document.getElementById("pivotPanelContainer"); var spreadHost = document.getElementById("ss"); if (isPivotPanelVisible) { panelContainer.classList.remove("pivot-panel-hidden"); spreadHost.style.width = "calc(100% - 600px)"; } else { panelContainer.classList.add("pivot-panel-hidden"); spreadHost.style.width = "calc(100% - 300px)"; } document.getElementById("togglePivotPanel").value = isPivotPanelVisible ? "피벗 패널 숨기기" : "피벗 패널 표시"; spread.refresh(); } function getDataTablePivotSource() { return { source: toPivotSourceText("TableSales"), autoRefresh: sourceStates.DataTable.autoRefresh }; } function getDataViewPivotSource() { return { source: toPivotSourceText("Sales") + "." + toPivotSourceText("SalesAnalysisView"), autoRefresh: sourceStates.DataView.autoRefresh }; } function getActivePivotSource() { if (getActiveSourceType() === "DataView") { return getDataViewPivotSource(); } return getDataTablePivotSource(); } function toPivotSourceText(name) { var calcEngine = GC.Spread.Sheets.CalcEngine; var expression = calcEngine.formulaToExpression(null, "SourceName"); expression.value = name; return calcEngine.expressionToFormula(null, expression); } function getActivePivotTable() { if (getActiveSourceType() === "DataView") { return viewPivotTable; } return tablePivotTable; } function getActiveSourceType() { return spread.getActiveSheetIndex() === 1 ? "DataView" : "DataTable"; } function getActiveState() { return sourceStates[getActiveSourceType()]; } function startFeed() { if (!timerId) { timerId = setInterval(addLiveRecord, 2000); addLiveRecord(); } } function pauseFeed() { if (timerId) { clearInterval(timerId); timerId = null; } updateDemoPanel(); } function resetData() { pauseFeed(); if (getActiveSourceType() === "DataView") { resetDataViewSource(); } else { resetDataTableSource(); } } function resetDataTableSource() { removeDataManagerTable("TableSales"); initDataTableSource().then(function () { spread.suspendPaint(); tablePivotTable.updateSource(getDataTablePivotSource()); spread.resumePaint(); updateDemoPanel(true); }); } function resetDataViewSource() { removeDataManagerTable("Sales"); removeDataManagerTable("Products"); removeDataManagerTable("SalesMen"); removeDataManagerTable("SalesRegions"); initDataViewSource().then(function () { spread.suspendPaint(); viewPivotTable.updateSource(getDataViewPivotSource()); spread.resumePaint(); updateDemoPanel(true); }); } function removeDataManagerTable(tableName) { var dataManager = spread.dataManager(); if (dataManager.tables && dataManager.tables[tableName]) { dataManager.removeTable(tableName); } } function addLiveRecord() { var sourceType = getActiveSourceType(); var state = sourceStates[sourceType]; var records = sourceType === "DataView" ? createViewLiveRecords(state.recordCount) : createTableLiveRecords(state.recordCount); var table = sourceType === "DataView" ? viewSalesTable : tableSalesTable; for (var i = 0; i < records.length; i++) { table.insertItem(state.recordCount + i, records[i]); } state.recordCount += records.length; updateDemoPanel(state.autoRefresh); } function createInitialTableSourceData() { var headers = pivotSales[0]; var data = []; for (var i = 1; i < pivotSales.length; i++) { var row = pivotSales[i]; var item = {}; for (var col = 0; col < headers.length; col++) { item[headers[col]] = row[col]; } item.price = item.price / 10; if (item.total === undefined) { item.total = item.quantity * item.price; } else { item.total = item.total / 10; } if (people.indexOf(item.salesperson) >= 0) { data.push(item); } } return data; } function createInitialViewSourceData() { var data = []; for (var i = 0; i < 60; i++) { data.push(createViewSalesRecord(i)); } return data; } function createTableLiveRecords(startIndex) { var records = []; for (var personIndex = 0; personIndex < people.length; personIndex++) { for (var carIndex = 0; carIndex < cars.length; carIndex++) { var car = cars[carIndex]; var index = startIndex + records.length; var quantity = 1 + ((index + personIndex + carIndex) % 8); records.push({ date: new Date(2020, index % 12, 1 + (index % 27)), salesperson: people[personIndex], car: car.name, quantity: quantity, price: car.price, total: quantity * car.price }); } } return records; } function createViewLiveRecords(startIndex) { var records = []; for (var i = 0; i < 20; i++) { records.push(createRandomViewSalesRecord(startIndex + i)); } return records; } function createViewSalesRecord(index) { var salesMan = salesMenData[index % salesMenData.length]; var productIds = salesManProductMap[salesMan.id] || productsData.map(function (product) { return product.id; }); var productId = productIds[Math.floor(index / salesMenData.length) % productIds.length]; var product = getProductById(productId); var quantity = 1 + (index % 9); return { date: new Date(2024, index % 12, 1 + (index % 27)), productId: productId, salesManId: salesMan.id, regionId: salesRegionsData[index % salesRegionsData.length].id, quantity: quantity, price: getSalesPrice(product, index) }; } function createRandomViewSalesRecord(index) { var salesMan = salesMenData[Math.floor(Math.random() * salesMenData.length)]; var productIds = salesManProductMap[salesMan.id] || productsData.map(function (product) { return product.id; }); var productId = productIds[Math.floor(Math.random() * productIds.length)]; var product = getProductById(productId); var quantity = 1 + (index % 9); return { date: new Date(2024, Math.floor(Math.random() * 12), 1 + Math.floor(Math.random() * 27)), productId: productId, salesManId: salesMan.id, regionId: salesRegionsData[Math.floor(Math.random() * salesRegionsData.length)].id, quantity: quantity, price: getSalesPrice(product, index) }; } function getProductById(productId) { for (var i = 0; i < productsData.length; i++) { if (productsData[i].id === productId) { return productsData[i]; } } return productsData[0]; } function getSalesPrice(product, index) { var markup = 1.12 + ((index % 5) * 0.04); return Math.round(product.price * markup); } function createSalesManProductMap() { var result = {}; var allProductIds = productsData.map(function (product) { return product.id; }); for (var i = 0; i < salesMenData.length; i++) { result[salesMenData[i].id] = []; } for (var productIndex = 0; productIndex < allProductIds.length; productIndex++) { result[salesMenData[productIndex % salesMenData.length].id].push(allProductIds[productIndex]); } for (var salesManIndex = 0; salesManIndex < salesMenData.length; salesManIndex++) { var salesMan = salesMenData[salesManIndex]; var products = allProductIds.slice(); shuffleArray(products); var targetCount = 3 + Math.floor(Math.random() * 3); for (var i = 0; i < products.length && result[salesMan.id].length < targetCount; i++) { if (result[salesMan.id].indexOf(products[i]) < 0) { result[salesMan.id].push(products[i]); } } } return result; } function shuffleArray(items) { for (var i = items.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = items[i]; items[i] = items[j]; items[j] = temp; } } function updateDemoPanel(pivotRefreshed) { var sourceType = getActiveSourceType(); var state = sourceStates[sourceType]; if (pivotRefreshed) { state.pivotRefreshTime = getTimeText(); } document.getElementById("panelTitle").textContent = sourceType + " 피벗 소스"; document.getElementById("sourceNote").textContent = getSourceNote(sourceType); document.getElementById("autoRefresh").checked = state.autoRefresh; document.getElementById("startFeed").disabled = !!timerId; document.getElementById("pauseFeed").disabled = !timerId; document.getElementById("manualRefresh").disabled = state.autoRefresh; document.getElementById("recordCount").textContent = state.recordCount.toLocaleString(); document.getElementById("lastPivotRefresh").textContent = state.pivotRefreshTime || "-"; } function markAllPivotRefreshed() { var timeText = getTimeText(); sourceStates.DataTable.pivotRefreshTime = timeText; sourceStates.DataView.pivotRefreshTime = timeText; } function getSourceNote(sourceType) { if (sourceType === "DataView") { return "DataView 피벗 소스는 하나의 팩트 테이블(Sales)과 세 개의 차원 테이블(Products, SalesMen, SalesRegions)을 사용합니다. SalesAnalysisView는 관계를 통해 이 테이블들을 조인합니다."; } return "DataTable 피벗 소스는 하나의 넓은 DataManager 테이블을 사용합니다. 영업 담당자와 제품 같은 판매 팩트 및 차원 이름이 동일한 테이블에 직접 저장됩니다."; } function getTimeText() { return new Date().toLocaleTimeString(); }
<!doctype html> <html style="height:100%;font-size:14px;"> <head> <meta name="spreadjs culture" content="ko-kr"/> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-shapes/dist/gc.spread.sheets.shapes.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-pivot-addon/dist/gc.spread.pivot.pivottables.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/data/pivot-data.js" type="text/javascript"></script> <script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-resources-ko/dist/gc.spread.sheets.resources.ko.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <div class="sample-tutorial"> <div id="ss" class="sample-spreadsheets"></div> <div id="pivotPanelContainer" class="sample-panel pivot-panel-hidden"> <div id="pivotPanel" class="pivot-panel"></div> </div> <div id="container" class="options-container"> <div class="dm-source-panel"> <div class="option-block"> <h3 id="panelTitle">DataTable 피벗 소스</h3> </div> <div class="option-block"> <div id="sourceNote" class="source-note"></div> </div> <div class="option-block checkbox-row"> <input type="checkbox" id="autoRefresh" checked> <label for="autoRefresh">피벗 테이블 자동 새로 고침</label> </div> <div class="button-grid"> <input type="button" value="실시간 업데이트 시작" id="startFeed"> <input type="button" value="일시 중지" id="pauseFeed"> <input type="button" value="수동 새로 고침" id="manualRefresh"> <input type="button" value="데이터 재설정" id="resetData"> </div> <div class="status-grid"> <div> <span>소스 레코드</span> <strong id="recordCount"></strong> </div> <div> <span>마지막 새로 고침</span> <strong id="lastPivotRefresh"></strong> </div> </div> <div class="option-block panel-toggle-block"> <input type="button" value="피벗 패널 표시" id="togglePivotPanel"> </div> </div> </div> </div> </body> </html>
html, body { height: 100%; margin: 0; } body { position: absolute; inset: 0; overflow: hidden; color: #1f2933; background: #f7f8fa; } .sample-tutorial { display: flex; height: 100%; overflow: hidden; } .sample-spreadsheets { flex: 1 1 auto; min-width: 0; height: 100%; overflow: hidden; } .sample-panel { flex: 0 0 300px; width: 300px; height: 100%; box-sizing: border-box; overflow: auto; background: #ffffff; border-left: 1px solid #e5e7eb; } .pivot-panel { width: 300px; height: 100%; box-sizing: border-box; } #pivotPanel.gc-panel, .pivot-panel .gc-panel { padding: 10px; background-color: #ffffff; border: none; } .sample-panel.pivot-panel-hidden { display: none; } .options-container { flex: 0 0 300px; width: 300px; height: 100%; padding: 24px 18px; box-sizing: border-box; overflow: auto; background: #ffffff; border-left: 1px solid #e5e7eb; } .dm-source-panel { min-height: 100%; display: flex; flex-direction: column; } .option-block { margin-bottom: 12px; } .option-block:first-child { margin-bottom: 0; } .option-block:first-child h3 { margin: 0; color: #111827; font-size: 20px; font-weight: 650; line-height: 1.25; } .option-block:nth-child(2) { padding: 12px 0 18px; margin-bottom: 0; border-bottom: 1px solid #eef0f3; } .source-note { margin: 0; color: #6b7280; font-size: 13px; line-height: 1.6; } .checkbox-row { display: flex; align-items: center; gap: 8px; padding: 18px 0 16px; margin-bottom: 0; border-bottom: 1px solid #eef0f3; } .checkbox-row input[type="checkbox"] { width: 16px; height: 16px; margin: 0; accent-color: #2563eb; flex: 0 0 auto; } .checkbox-row label { margin: 0; color: #374151; font-size: 13px; font-weight: 600; line-height: 1.35; } .button-grid, .status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } .button-grid { grid-auto-rows: 34px; padding: 18px 0; margin-bottom: 0; border-bottom: 1px solid #eef0f3; } .status-grid { padding: 18px 0; margin-bottom: 0; } .status-grid div { padding: 10px 12px; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 6px; box-sizing: border-box; } .status-grid span { display: block; margin-bottom: 6px; color: #6b7280; font-size: 12px; } .status-grid strong { display: block; color: #1f2933; font-size: 14px; } .panel-toggle-block { margin-top: auto; margin-bottom: 0; padding-top: 18px; border-top: 1px solid #eef0f3; } .options-container input[type="button"] { display: block; width: 100%; height: 100%; min-height: 34px; padding: 0 10px; box-sizing: border-box; line-height: 1; white-space: nowrap; color: #1f4f82; font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; background: #fff; border: 1px solid #9bb2cf; border-radius: 4px; transition: background-color .15s ease, border-color .15s ease; } .options-container input[type="button"]:hover { background: #edf5ff; border-color: #6f98c8; } .options-container input[type="button"]:disabled { cursor: default; opacity: 0.55; } .options-container input[type="button"]:disabled:hover { background: #fff; border-color: #9bb2cf; } @media (max-width: 760px) { .sample-tutorial { flex-direction: column; } .sample-spreadsheets { min-height: 55%; } .sample-panel, .options-container { flex: 0 0 auto; width: 100%; height: 45%; border-left: 0; border-top: 1px solid #e5e7eb; } }