Add finance 3D data analysis
This commit is contained in:
@@ -23,6 +23,8 @@
|
||||
</script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="js/download.js"></script>
|
||||
<script src="js/vendor/three.min.js"></script>
|
||||
<script src="js/finance3d.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
<MudNavLink Href="management-cockpit?section=division&division=central" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.AccountTree">
|
||||
@T("Zentrale Spartenzuordnung", "Central division mapping")
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="management-cockpit?section=3d" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.ViewInAr">
|
||||
@T("3D Datenanalyse", "3D data analysis")
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="management-cockpit?section=raw" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.QueryStats">
|
||||
@T("Rohdaten Diagnose", "Raw-data diagnostics")
|
||||
</MudNavLink>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
@page "/management-cockpit"
|
||||
@rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer)
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.JSInterop
|
||||
@using TrafagSalesExporter.Models
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IManagementCockpitPageService CockpitPageService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IUiTextService UiText
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<PageTitle>@T("Management Analyse", "Management analysis")</PageTitle>
|
||||
|
||||
@@ -648,6 +650,30 @@
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("3D Datenanalyse", "3D data analysis")" Icon="@Icons.Material.Filled.ViewInAr">
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudSelect T="string" Value="_finance3dIndicator" ValueChanged="SetFinance3dIndicator" Label="@T("Indikator", "Indicator")" Dense>
|
||||
@foreach (var option in _finance3dIndicatorOptions)
|
||||
{
|
||||
<MudSelectItem Value="@option.Key">@T(option.GermanLabel, option.EnglishLabel)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="9">
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("Drehbar mit gedrueckter Maus, Zoom mit Mausrad. X-Achse = Land, Z-Achse = Jahr, Hoehe = gewaehlter Indikator.",
|
||||
"Drag with the mouse to rotate, use the mouse wheel to zoom. X axis = country, Z axis = year, height = selected indicator.")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-0 finance-3d-surface" Elevation="1">
|
||||
<canvas @ref="_finance3dCanvas" class="finance-3d-canvas"></canvas>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("Rohdaten Diagnose", "Raw-data diagnostics")" Icon="@Icons.Material.Filled.QueryStats">
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
@@ -1015,6 +1041,13 @@
|
||||
new(ProductFinanceGroupLevels.Family, "Produktfamilie", "Product family"),
|
||||
new(ProductFinanceGroupLevels.Division, "Produktsparte", "Product division")
|
||||
];
|
||||
private readonly List<Finance3dIndicatorOption> _finance3dIndicatorOptions =
|
||||
[
|
||||
new(Finance3dIndicators.Actual, "Net Sales Actual", "Net sales actual"),
|
||||
new(Finance3dIndicators.IncludedRows, "Enthaltene Zeilen", "Included rows"),
|
||||
new(Finance3dIndicators.ExcludedRows, "Ausgeschlossene Zeilen", "Excluded rows"),
|
||||
new(Finance3dIndicators.Deviation, "Soll/Ist Differenz Filterjahr", "Actual/reference difference filter year")
|
||||
];
|
||||
private string? _selectedFilePath;
|
||||
private ManagementCockpitResult? _result;
|
||||
private ManagementCockpitCentralResult? _centralResult;
|
||||
@@ -1040,6 +1073,9 @@
|
||||
private int _activeDivisionTabIndex;
|
||||
private string _productFinanceGroupLevel = ProductFinanceGroupLevels.Hierarchy;
|
||||
private bool _limitProductFinanceTop10;
|
||||
private string _finance3dIndicator = Finance3dIndicators.Actual;
|
||||
private ElementReference _finance3dCanvas;
|
||||
private bool _finance3dNeedsRender;
|
||||
|
||||
private bool ShowProductFamilyColumn => _productFinanceGroupLevel != ProductFinanceGroupLevels.Division;
|
||||
|
||||
@@ -1068,6 +1104,7 @@
|
||||
"deviations" => ManagementFinanceTabIndexes.Deviations,
|
||||
"credits" => ManagementFinanceTabIndexes.Credits,
|
||||
"quality" => ManagementFinanceTabIndexes.Quality,
|
||||
"3d" => ManagementFinanceTabIndexes.ThreeD,
|
||||
"raw" => ManagementFinanceTabIndexes.Raw,
|
||||
_ => ManagementFinanceTabIndexes.Summary
|
||||
};
|
||||
@@ -1086,6 +1123,15 @@
|
||||
await AnalyzeFinanceSummary();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (_finance3dNeedsRender && _financeResult is not null)
|
||||
{
|
||||
_finance3dNeedsRender = false;
|
||||
await RenderFinance3dAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadFiles()
|
||||
{
|
||||
_loadingFiles = true;
|
||||
@@ -1174,6 +1220,7 @@
|
||||
_financeCountryOptions = _financeResult.CountryOptions;
|
||||
_financeCurrencyOptions = _financeResult.CurrencyOptions;
|
||||
_selectedFinanceYear = _financeResult.Filter.Year;
|
||||
_finance3dNeedsRender = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1196,6 +1243,71 @@
|
||||
_limitProductFinanceTop10 = !_limitProductFinanceTop10;
|
||||
}
|
||||
|
||||
private async Task SetFinance3dIndicator(string value)
|
||||
{
|
||||
_finance3dIndicator = string.IsNullOrWhiteSpace(value) ? Finance3dIndicators.Actual : value;
|
||||
await RenderFinance3dAsync();
|
||||
}
|
||||
|
||||
private async Task RenderFinance3dAsync()
|
||||
{
|
||||
if (_financeResult is null)
|
||||
return;
|
||||
|
||||
var rows = BuildFinance3dRows();
|
||||
await JsRuntime.InvokeVoidAsync("trafagFinance3d.render", _finance3dCanvas, rows, new
|
||||
{
|
||||
indicator = _finance3dIndicator,
|
||||
title = ResolveFinance3dIndicatorLabel(_finance3dIndicator)
|
||||
});
|
||||
}
|
||||
|
||||
private IReadOnlyList<object> BuildFinance3dRows()
|
||||
{
|
||||
if (_financeResult is null)
|
||||
return [];
|
||||
|
||||
var deviationsByKey = _financeResult.CountryRows
|
||||
.Where(row => row.Difference.HasValue)
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Sum(row => Math.Abs(row.Difference!.Value)));
|
||||
|
||||
var sourceRows = _finance3dIndicator == Finance3dIndicators.Deviation
|
||||
? _financeResult.Rows
|
||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
||||
|
||||
return sourceRows
|
||||
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(row => row.Year)
|
||||
.Select(row =>
|
||||
{
|
||||
deviationsByKey.TryGetValue($"{row.Year}|{row.CountryKey}", out var deviation);
|
||||
var value = _finance3dIndicator switch
|
||||
{
|
||||
Finance3dIndicators.IncludedRows => row.IncludedRows,
|
||||
Finance3dIndicators.ExcludedRows => row.ExcludedRows,
|
||||
Finance3dIndicators.Deviation => deviation,
|
||||
_ => Math.Abs(row.NetSalesActual)
|
||||
};
|
||||
return new
|
||||
{
|
||||
country = row.CountryKey,
|
||||
year = row.Year,
|
||||
currency = row.Currency,
|
||||
value
|
||||
};
|
||||
})
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private string ResolveFinance3dIndicatorLabel(string key)
|
||||
=> _finance3dIndicatorOptions.FirstOrDefault(option => option.Key == key) is { } option
|
||||
? T(option.GermanLabel, option.EnglishLabel)
|
||||
: T("Net Sales Actual", "Net sales actual");
|
||||
|
||||
private IReadOnlyList<ManagementProductDivisionFinanceRow> BuildProductFinanceRows()
|
||||
{
|
||||
if (_financeResult is null)
|
||||
@@ -1495,10 +1607,20 @@
|
||||
public const int Credits = 4;
|
||||
public const int Quality = 5;
|
||||
public const int Division = 6;
|
||||
public const int Raw = 7;
|
||||
public const int ThreeD = 7;
|
||||
public const int Raw = 8;
|
||||
}
|
||||
|
||||
private static class Finance3dIndicators
|
||||
{
|
||||
public const string Actual = "actual";
|
||||
public const string IncludedRows = "includedRows";
|
||||
public const string ExcludedRows = "excludedRows";
|
||||
public const string Deviation = "deviation";
|
||||
}
|
||||
|
||||
private sealed record ProductFinanceGroupingOption(string Key, string GermanLabel, string EnglishLabel);
|
||||
private sealed record Finance3dIndicatorOption(string Key, string GermanLabel, string EnglishLabel);
|
||||
|
||||
private sealed record ProductFinanceGroupKey(
|
||||
string ProductDivisionCode,
|
||||
|
||||
@@ -314,6 +314,7 @@ public class ManagementFinanceSummaryResult
|
||||
public List<string> CurrencyOptions { get; set; } = [];
|
||||
public List<ManagementFinanceSummaryRow> Rows { get; set; } = [];
|
||||
public List<ManagementFinanceSummaryRow> YearRows { get; set; } = [];
|
||||
public List<ManagementFinanceSummaryRow> YearCountryRows { get; set; } = [];
|
||||
public int IncludedRows { get; set; }
|
||||
public int ExcludedRows { get; set; }
|
||||
public int CountryCount { get; set; }
|
||||
|
||||
@@ -443,6 +443,16 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
.Select(group => BuildFinanceSummaryRow(group.Key.Year, "Alle", group.Key.Currency, group))
|
||||
.ToList();
|
||||
|
||||
var yearCountryRows = allRows
|
||||
.Where(row => countryFilter is null || row.CountryKey.Equals(countryFilter, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(row => currencyFilter is null || row.Currency.Equals(currencyFilter, StringComparison.OrdinalIgnoreCase))
|
||||
.GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
|
||||
.OrderBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(group => group.Key.Year)
|
||||
.ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => BuildFinanceSummaryRow(group.Key.Year, group.Key.CountryKey, group.Key.Currency, group))
|
||||
.ToList();
|
||||
|
||||
var includedRows = scopedRows.Count(row => row.Include);
|
||||
var excludedRows = scopedRows.Count(row => !row.Include);
|
||||
var resultCurrencies = summaryRows
|
||||
@@ -501,6 +511,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
.ToList(),
|
||||
Rows = summaryRows,
|
||||
YearRows = yearRows,
|
||||
YearCountryRows = yearCountryRows,
|
||||
IncludedRows = includedRows,
|
||||
ExcludedRows = excludedRows,
|
||||
CountryCount = summaryRows.Select(row => row.CountryKey).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
|
||||
|
||||
@@ -39,3 +39,27 @@ body,
|
||||
.language-button .mud-button-icon-end {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.finance-3d-surface {
|
||||
width: 100%;
|
||||
min-height: 560px;
|
||||
overflow: hidden;
|
||||
background: #f7f9fb;
|
||||
}
|
||||
|
||||
.finance-3d-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 560px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.finance-3d-surface {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.finance-3d-canvas {
|
||||
height: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
(function () {
|
||||
const stateByCanvas = new WeakMap();
|
||||
|
||||
function normalizeRows(rows) {
|
||||
return Array.isArray(rows) ? rows.filter(row => row && Number.isFinite(Number(row.value))) : [];
|
||||
}
|
||||
|
||||
function buildAxes(rows) {
|
||||
const countries = [...new Set(rows.map(row => String(row.country || "-")))].sort();
|
||||
const years = [...new Set(rows.map(row => Number(row.year || 0)))].sort((a, b) => a - b);
|
||||
const maxValue = rows.reduce((max, row) => Math.max(max, Math.abs(Number(row.value || 0))), 0) || 1;
|
||||
return { countries, years, maxValue };
|
||||
}
|
||||
|
||||
function createThreeScene(canvas, rows, options) {
|
||||
const THREE = window.THREE;
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
renderer.setClearColor(0xf7f9fb, 1);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
|
||||
camera.position.set(18, 15, 22);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
const root = new THREE.Group();
|
||||
scene.add(root);
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.72));
|
||||
const light = new THREE.DirectionalLight(0xffffff, 0.75);
|
||||
light.position.set(8, 18, 10);
|
||||
scene.add(light);
|
||||
|
||||
const axes = buildAxes(rows);
|
||||
const xStep = axes.countries.length > 1 ? 16 / (axes.countries.length - 1) : 0;
|
||||
const zStep = axes.years.length > 1 ? 12 / (axes.years.length - 1) : 0;
|
||||
const xStart = -8;
|
||||
const zStart = -6;
|
||||
|
||||
const gridMaterial = new THREE.LineBasicMaterial({ color: 0xb7c1cc, transparent: true, opacity: 0.55 });
|
||||
const gridPoints = [];
|
||||
for (let i = 0; i <= axes.countries.length; i++) {
|
||||
const x = xStart + (i - 0.5) * (xStep || 2);
|
||||
gridPoints.push(new THREE.Vector3(x, 0, zStart - 1), new THREE.Vector3(x, 0, zStart + Math.max(1, axes.years.length - 1) * (zStep || 2) + 1));
|
||||
}
|
||||
for (let i = 0; i <= axes.years.length; i++) {
|
||||
const z = zStart + (i - 0.5) * (zStep || 2);
|
||||
gridPoints.push(new THREE.Vector3(xStart - 1, 0, z), new THREE.Vector3(xStart + Math.max(1, axes.countries.length - 1) * (xStep || 2) + 1, 0, z));
|
||||
}
|
||||
root.add(new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(gridPoints), gridMaterial));
|
||||
|
||||
const barGeometry = new THREE.BoxGeometry(0.68, 1, 0.68);
|
||||
rows.forEach(row => {
|
||||
const countryIndex = Math.max(0, axes.countries.indexOf(String(row.country || "-")));
|
||||
const yearIndex = Math.max(0, axes.years.indexOf(Number(row.year || 0)));
|
||||
const rawValue = Math.abs(Number(row.value || 0));
|
||||
const height = Math.max(0.08, rawValue / axes.maxValue * 8);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: colorForValue(rawValue / axes.maxValue),
|
||||
roughness: 0.58,
|
||||
metalness: 0.05
|
||||
});
|
||||
const bar = new THREE.Mesh(barGeometry, material);
|
||||
bar.scale.y = height;
|
||||
bar.position.set(xStart + countryIndex * (xStep || 2), height / 2, zStart + yearIndex * (zStep || 2));
|
||||
root.add(bar);
|
||||
});
|
||||
|
||||
addCanvasLabel(scene, THREE, options.title || "", -8.8, 9.2, -7.8, 1.05);
|
||||
axes.countries.forEach((country, index) => addCanvasLabel(scene, THREE, country, xStart + index * (xStep || 2), -0.15, zStart - 1.3, 0.58));
|
||||
axes.years.forEach((year, index) => addCanvasLabel(scene, THREE, String(year), xStart - 1.6, -0.15, zStart + index * (zStep || 2), 0.58));
|
||||
|
||||
const state = { renderer, scene, camera, root, angleX: -0.62, angleY: 0.78, distance: 30, dragging: false, lastX: 0, lastY: 0 };
|
||||
attachInteraction(canvas, state);
|
||||
stateByCanvas.set(canvas, state);
|
||||
resizeAndRender(canvas);
|
||||
}
|
||||
|
||||
function addCanvasLabel(scene, THREE, text, x, y, z, scale) {
|
||||
const labelCanvas = document.createElement("canvas");
|
||||
labelCanvas.width = 512;
|
||||
labelCanvas.height = 128;
|
||||
const ctx = labelCanvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, labelCanvas.width, labelCanvas.height);
|
||||
ctx.fillStyle = "#243241";
|
||||
ctx.font = "600 44px Open Sans, Arial, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(String(text || "-"), 256, 64, 480);
|
||||
const texture = new THREE.CanvasTexture(labelCanvas);
|
||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.position.set(x, y, z);
|
||||
sprite.scale.set(3.5 * scale, 0.85 * scale, 1);
|
||||
scene.add(sprite);
|
||||
}
|
||||
|
||||
function colorForValue(t) {
|
||||
const clamped = Math.max(0, Math.min(1, t));
|
||||
const r = Math.round(45 + clamped * 178);
|
||||
const g = Math.round(105 + clamped * 78);
|
||||
const b = Math.round(155 - clamped * 88);
|
||||
return (r << 16) + (g << 8) + b;
|
||||
}
|
||||
|
||||
function attachInteraction(canvas, state) {
|
||||
canvas.onpointerdown = event => {
|
||||
state.dragging = true;
|
||||
state.lastX = event.clientX;
|
||||
state.lastY = event.clientY;
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
};
|
||||
canvas.onpointermove = event => {
|
||||
if (!state.dragging) return;
|
||||
const dx = event.clientX - state.lastX;
|
||||
const dy = event.clientY - state.lastY;
|
||||
state.lastX = event.clientX;
|
||||
state.lastY = event.clientY;
|
||||
state.angleY += dx * 0.008;
|
||||
state.angleX = Math.max(-1.25, Math.min(-0.15, state.angleX + dy * 0.006));
|
||||
renderState(state, canvas);
|
||||
};
|
||||
canvas.onpointerup = event => {
|
||||
state.dragging = false;
|
||||
try { canvas.releasePointerCapture(event.pointerId); } catch { }
|
||||
};
|
||||
canvas.onwheel = event => {
|
||||
event.preventDefault();
|
||||
state.distance = Math.max(16, Math.min(54, state.distance + event.deltaY * 0.025));
|
||||
renderState(state, canvas);
|
||||
};
|
||||
}
|
||||
|
||||
function renderState(state, canvas) {
|
||||
const width = canvas.clientWidth || 900;
|
||||
const height = canvas.clientHeight || 520;
|
||||
state.camera.aspect = width / height;
|
||||
state.camera.updateProjectionMatrix();
|
||||
const horizontal = Math.cos(state.angleX) * state.distance;
|
||||
state.camera.position.set(Math.sin(state.angleY) * horizontal, Math.sin(-state.angleX) * state.distance, Math.cos(state.angleY) * horizontal);
|
||||
state.camera.lookAt(0, 2.8, 0);
|
||||
state.renderer.setSize(width, height, false);
|
||||
state.renderer.render(state.scene, state.camera);
|
||||
}
|
||||
|
||||
function renderFallback(canvas, rows, options) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
ctx.fillStyle = "#f7f9fb";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.fillStyle = "#243241";
|
||||
ctx.font = "600 16px Open Sans, Arial, sans-serif";
|
||||
ctx.fillText(options.title || "3D data analysis", 18, 28);
|
||||
|
||||
const axes = buildAxes(rows);
|
||||
const barWidth = Math.max(12, Math.min(42, (width - 80) / Math.max(1, rows.length) * 0.72));
|
||||
rows.forEach((row, index) => {
|
||||
const value = Math.abs(Number(row.value || 0));
|
||||
const barHeight = value / axes.maxValue * (height - 110);
|
||||
const x = 44 + index * ((width - 90) / Math.max(1, rows.length));
|
||||
const y = height - 42 - barHeight;
|
||||
ctx.fillStyle = `hsl(${205 - (value / axes.maxValue) * 115}, 58%, 45%)`;
|
||||
ctx.fillRect(x, y, barWidth, barHeight);
|
||||
});
|
||||
ctx.fillStyle = "#52606d";
|
||||
ctx.font = "12px Open Sans, Arial, sans-serif";
|
||||
ctx.fillText("Fallback canvas renderer: Three.js could not be loaded.", 18, height - 16);
|
||||
}
|
||||
|
||||
function resizeAndRender(canvas) {
|
||||
const state = stateByCanvas.get(canvas);
|
||||
if (state) renderState(state, canvas);
|
||||
}
|
||||
|
||||
window.trafagFinance3d = {
|
||||
render: function (canvas, rows, options) {
|
||||
if (!canvas) return;
|
||||
const normalizedRows = normalizeRows(rows);
|
||||
if (normalizedRows.length === 0) return;
|
||||
const existing = stateByCanvas.get(canvas);
|
||||
if (existing && existing.renderer && existing.renderer.dispose) {
|
||||
existing.renderer.dispose();
|
||||
}
|
||||
if (window.THREE && window.THREE.WebGLRenderer) {
|
||||
createThreeScene(canvas, normalizedRows, options || {});
|
||||
} else {
|
||||
renderFallback(canvas, normalizedRows, options || {});
|
||||
}
|
||||
},
|
||||
resize: resizeAndRender,
|
||||
pixelProbe: function (canvas) {
|
||||
const ctx = canvas && canvas.getContext ? canvas.getContext("2d", { willReadFrequently: true }) : null;
|
||||
if (!ctx) return -1;
|
||||
const data = ctx.getImageData(0, 0, Math.min(32, canvas.width), Math.min(32, canvas.height)).data;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < data.length; i += 4) sum += data[i] + data[i + 1] + data[i + 2];
|
||||
return sum;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
document.querySelectorAll(".finance-3d-canvas").forEach(canvas => resizeAndRender(canvas));
|
||||
});
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user