Improve finance 3D controls and simulation
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@page "/management-cockpit"
|
||||
@rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer)
|
||||
@using System.Globalization
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.JSInterop
|
||||
@using TrafagSalesExporter.Models
|
||||
@@ -661,10 +662,44 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="9">
|
||||
<MudItem xs="12" md="4">
|
||||
<MudText Typo="Typo.caption">@T("Szenario-Faktor / Wechselkurs", "Scenario factor / exchange rate")</MudText>
|
||||
<div class="finance-3d-range-row">
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => SetFinance3dScenarioFactorPreset(0.9d))">-10%</MudButton>
|
||||
<input class="finance-3d-range"
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.01"
|
||||
value="@_finance3dScenarioFactor.ToString("0.00", CultureInfo.InvariantCulture)"
|
||||
@oninput="SetFinance3dScenarioFactor" />
|
||||
<MudText Typo="Typo.body2" Class="finance-3d-factor">@_finance3dScenarioFactor.ToString("0.00", CultureInfo.InvariantCulture)x</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.RestartAlt"
|
||||
Size="Size.Small"
|
||||
Color="Color.Default"
|
||||
OnClick="ResetFinance3dScenarioFactor" />
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => SetFinance3dScenarioFactorPreset(1.1d))">+10%</MudButton>
|
||||
</div>
|
||||
@if (Finance3dScenarioAffectsValue)
|
||||
{
|
||||
<MudText Typo="Typo.caption">
|
||||
@T("Basis", "Base"): @FormatFinance3dValue(Finance3dBaseTotal) |
|
||||
@T("Szenario", "Scenario"): @FormatFinance3dValue(Finance3dScenarioTotal) |
|
||||
@T("Delta", "Delta"): @FormatFinance3dValue(Finance3dScenarioDelta)
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption">
|
||||
@T("Der Faktor wirkt nur auf Wertindikatoren, nicht auf Zeilenanzahlen.",
|
||||
"The factor only affects value indicators, not row counts.")
|
||||
</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="5">
|
||||
<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.")
|
||||
@T("Linke Maustaste drehen, Mausrad zoomen, Shift+Ziehen oder rechte Maustaste verschieben. X-Achse = Land, Z-Achse = Jahr, Hoehe = gewaehlter Indikator.",
|
||||
"Left mouse button rotates, mouse wheel zooms, Shift+drag or right mouse button pans. X axis = country, Z axis = year, height = selected indicator.")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -1074,10 +1109,17 @@
|
||||
private string _productFinanceGroupLevel = ProductFinanceGroupLevels.Hierarchy;
|
||||
private bool _limitProductFinanceTop10;
|
||||
private string _finance3dIndicator = Finance3dIndicators.Actual;
|
||||
private double _finance3dScenarioFactor = 1d;
|
||||
private ElementReference _finance3dCanvas;
|
||||
private bool _finance3dNeedsRender;
|
||||
|
||||
private bool ShowProductFamilyColumn => _productFinanceGroupLevel != ProductFinanceGroupLevels.Division;
|
||||
private bool Finance3dScenarioAffectsValue => _finance3dIndicator is Finance3dIndicators.Actual or Finance3dIndicators.Deviation;
|
||||
private decimal Finance3dBaseTotal => CalculateFinance3dBaseTotal();
|
||||
private decimal Finance3dScenarioTotal => Finance3dScenarioAffectsValue
|
||||
? Finance3dBaseTotal * (decimal)_finance3dScenarioFactor
|
||||
: Finance3dBaseTotal;
|
||||
private decimal Finance3dScenarioDelta => Finance3dScenarioTotal - Finance3dBaseTotal;
|
||||
|
||||
private bool ShowProductHierarchyColumn => _productFinanceGroupLevel == ProductFinanceGroupLevels.Hierarchy;
|
||||
|
||||
@@ -1249,6 +1291,27 @@
|
||||
await RenderFinance3dAsync();
|
||||
}
|
||||
|
||||
private async Task SetFinance3dScenarioFactor(ChangeEventArgs args)
|
||||
{
|
||||
if (double.TryParse(Convert.ToString(args.Value, CultureInfo.InvariantCulture), NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
_finance3dScenarioFactor = Math.Clamp(value, 0.5d, 1.5d);
|
||||
await RenderFinance3dAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetFinance3dScenarioFactor()
|
||||
{
|
||||
_finance3dScenarioFactor = 1d;
|
||||
await RenderFinance3dAsync();
|
||||
}
|
||||
|
||||
private async Task SetFinance3dScenarioFactorPreset(double value)
|
||||
{
|
||||
_finance3dScenarioFactor = Math.Clamp(value, 0.5d, 1.5d);
|
||||
await RenderFinance3dAsync();
|
||||
}
|
||||
|
||||
private async Task RenderFinance3dAsync()
|
||||
{
|
||||
if (_financeResult is null)
|
||||
@@ -1288,8 +1351,8 @@
|
||||
{
|
||||
Finance3dIndicators.IncludedRows => row.IncludedRows,
|
||||
Finance3dIndicators.ExcludedRows => row.ExcludedRows,
|
||||
Finance3dIndicators.Deviation => deviation,
|
||||
_ => Math.Abs(row.NetSalesActual)
|
||||
Finance3dIndicators.Deviation => deviation * (decimal)_finance3dScenarioFactor,
|
||||
_ => Math.Abs(row.NetSalesActual) * (decimal)_finance3dScenarioFactor
|
||||
};
|
||||
return new
|
||||
{
|
||||
@@ -1303,6 +1366,33 @@
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private decimal CalculateFinance3dBaseTotal()
|
||||
{
|
||||
if (_financeResult is null)
|
||||
return 0m;
|
||||
|
||||
var sourceRows = _finance3dIndicator == Finance3dIndicators.Deviation
|
||||
? _financeResult.Rows
|
||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
||||
|
||||
if (_finance3dIndicator == Finance3dIndicators.Deviation)
|
||||
{
|
||||
return _financeResult.CountryRows
|
||||
.Where(row => row.Difference.HasValue)
|
||||
.Sum(row => Math.Abs(row.Difference!.Value));
|
||||
}
|
||||
|
||||
return _finance3dIndicator switch
|
||||
{
|
||||
Finance3dIndicators.IncludedRows => sourceRows.Sum(row => row.IncludedRows),
|
||||
Finance3dIndicators.ExcludedRows => sourceRows.Sum(row => row.ExcludedRows),
|
||||
_ => sourceRows.Sum(row => Math.Abs(row.NetSalesActual))
|
||||
};
|
||||
}
|
||||
|
||||
private string FormatFinance3dValue(decimal value)
|
||||
=> value.ToString("N0", CultureInfo.CurrentCulture);
|
||||
|
||||
private string ResolveFinance3dIndicatorLabel(string key)
|
||||
=> _finance3dIndicatorOptions.FirstOrDefault(option => option.Key == key) is { } option
|
||||
? T(option.GermanLabel, option.EnglishLabel)
|
||||
|
||||
@@ -54,6 +54,23 @@ body,
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.finance-3d-range-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.finance-3d-range {
|
||||
flex: 1 1 auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.finance-3d-factor {
|
||||
min-width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.finance-3d-surface {
|
||||
min-height: 420px;
|
||||
|
||||
@@ -69,7 +69,23 @@
|
||||
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 };
|
||||
const previous = stateByCanvas.get(canvas);
|
||||
const state = {
|
||||
renderer,
|
||||
scene,
|
||||
camera,
|
||||
root,
|
||||
angleX: previous ? previous.angleX : -0.62,
|
||||
angleY: previous ? previous.angleY : 0.78,
|
||||
distance: previous ? previous.distance : 30,
|
||||
targetX: previous ? previous.targetX : 0,
|
||||
targetY: previous ? previous.targetY : 2.8,
|
||||
targetZ: previous ? previous.targetZ : 0,
|
||||
dragging: false,
|
||||
dragMode: "rotate",
|
||||
lastX: 0,
|
||||
lastY: 0
|
||||
};
|
||||
attachInteraction(canvas, state);
|
||||
stateByCanvas.set(canvas, state);
|
||||
resizeAndRender(canvas);
|
||||
@@ -104,7 +120,9 @@
|
||||
|
||||
function attachInteraction(canvas, state) {
|
||||
canvas.onpointerdown = event => {
|
||||
event.preventDefault();
|
||||
state.dragging = true;
|
||||
state.dragMode = event.button === 2 || event.button === 1 || event.shiftKey ? "pan" : "rotate";
|
||||
state.lastX = event.clientX;
|
||||
state.lastY = event.clientY;
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
@@ -115,29 +133,66 @@
|
||||
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));
|
||||
if (state.dragMode === "pan") {
|
||||
panCamera(state, dx, dy);
|
||||
} else {
|
||||
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.onpointercancel = () => {
|
||||
state.dragging = false;
|
||||
};
|
||||
canvas.oncontextmenu = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
canvas.onwheel = event => {
|
||||
event.preventDefault();
|
||||
state.distance = Math.max(16, Math.min(54, state.distance + event.deltaY * 0.025));
|
||||
const delta = normalizeWheelDelta(event);
|
||||
const zoomFactor = delta > 0 ? 1.12 : 0.88;
|
||||
state.distance = Math.max(14, Math.min(62, state.distance * zoomFactor));
|
||||
renderState(state, canvas);
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWheelDelta(event) {
|
||||
if (Number.isFinite(event.deltaY) && event.deltaY !== 0) {
|
||||
return event.deltaY > 0 ? 1 : -1;
|
||||
}
|
||||
if (Number.isFinite(event.wheelDelta) && event.wheelDelta !== 0) {
|
||||
return event.wheelDelta < 0 ? 1 : -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function panCamera(state, dx, dy) {
|
||||
const scale = state.distance * 0.0018;
|
||||
const rightX = Math.cos(state.angleY);
|
||||
const rightZ = -Math.sin(state.angleY);
|
||||
const forwardX = Math.sin(state.angleY);
|
||||
const forwardZ = Math.cos(state.angleY);
|
||||
state.targetX -= dx * scale * rightX;
|
||||
state.targetZ -= dx * scale * rightZ;
|
||||
state.targetX += dy * scale * forwardX;
|
||||
state.targetZ += dy * scale * forwardZ;
|
||||
}
|
||||
|
||||
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.camera.position.set(
|
||||
state.targetX + Math.sin(state.angleY) * horizontal,
|
||||
state.targetY + Math.sin(-state.angleX) * state.distance,
|
||||
state.targetZ + Math.cos(state.angleY) * horizontal);
|
||||
state.camera.lookAt(state.targetX, state.targetY, state.targetZ);
|
||||
state.renderer.setSize(width, height, false);
|
||||
state.renderer.render(state.scene, state.camera);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user