Improve finance 3D controls and simulation
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
@page "/management-cockpit"
|
@page "/management-cockpit"
|
||||||
@rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer)
|
@rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer)
|
||||||
|
@using System.Globalization
|
||||||
@using Microsoft.AspNetCore.Components
|
@using Microsoft.AspNetCore.Components
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@@ -661,10 +662,44 @@
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</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">
|
<MudText Typo="Typo.body2">
|
||||||
@T("Drehbar mit gedrueckter Maus, Zoom mit Mausrad. X-Achse = Land, Z-Achse = Jahr, Hoehe = gewaehlter Indikator.",
|
@T("Linke Maustaste drehen, Mausrad zoomen, Shift+Ziehen oder rechte Maustaste verschieben. 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.")
|
"Left mouse button rotates, mouse wheel zooms, Shift+drag or right mouse button pans. X axis = country, Z axis = year, height = selected indicator.")
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
@@ -1074,10 +1109,17 @@
|
|||||||
private string _productFinanceGroupLevel = ProductFinanceGroupLevels.Hierarchy;
|
private string _productFinanceGroupLevel = ProductFinanceGroupLevels.Hierarchy;
|
||||||
private bool _limitProductFinanceTop10;
|
private bool _limitProductFinanceTop10;
|
||||||
private string _finance3dIndicator = Finance3dIndicators.Actual;
|
private string _finance3dIndicator = Finance3dIndicators.Actual;
|
||||||
|
private double _finance3dScenarioFactor = 1d;
|
||||||
private ElementReference _finance3dCanvas;
|
private ElementReference _finance3dCanvas;
|
||||||
private bool _finance3dNeedsRender;
|
private bool _finance3dNeedsRender;
|
||||||
|
|
||||||
private bool ShowProductFamilyColumn => _productFinanceGroupLevel != ProductFinanceGroupLevels.Division;
|
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;
|
private bool ShowProductHierarchyColumn => _productFinanceGroupLevel == ProductFinanceGroupLevels.Hierarchy;
|
||||||
|
|
||||||
@@ -1249,6 +1291,27 @@
|
|||||||
await RenderFinance3dAsync();
|
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()
|
private async Task RenderFinance3dAsync()
|
||||||
{
|
{
|
||||||
if (_financeResult is null)
|
if (_financeResult is null)
|
||||||
@@ -1288,8 +1351,8 @@
|
|||||||
{
|
{
|
||||||
Finance3dIndicators.IncludedRows => row.IncludedRows,
|
Finance3dIndicators.IncludedRows => row.IncludedRows,
|
||||||
Finance3dIndicators.ExcludedRows => row.ExcludedRows,
|
Finance3dIndicators.ExcludedRows => row.ExcludedRows,
|
||||||
Finance3dIndicators.Deviation => deviation,
|
Finance3dIndicators.Deviation => deviation * (decimal)_finance3dScenarioFactor,
|
||||||
_ => Math.Abs(row.NetSalesActual)
|
_ => Math.Abs(row.NetSalesActual) * (decimal)_finance3dScenarioFactor
|
||||||
};
|
};
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
@@ -1303,6 +1366,33 @@
|
|||||||
.ToList();
|
.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)
|
private string ResolveFinance3dIndicatorLabel(string key)
|
||||||
=> _finance3dIndicatorOptions.FirstOrDefault(option => option.Key == key) is { } option
|
=> _finance3dIndicatorOptions.FirstOrDefault(option => option.Key == key) is { } option
|
||||||
? T(option.GermanLabel, option.EnglishLabel)
|
? T(option.GermanLabel, option.EnglishLabel)
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ body,
|
|||||||
touch-action: none;
|
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) {
|
@media (max-width: 700px) {
|
||||||
.finance-3d-surface {
|
.finance-3d-surface {
|
||||||
min-height: 420px;
|
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.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));
|
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);
|
attachInteraction(canvas, state);
|
||||||
stateByCanvas.set(canvas, state);
|
stateByCanvas.set(canvas, state);
|
||||||
resizeAndRender(canvas);
|
resizeAndRender(canvas);
|
||||||
@@ -104,7 +120,9 @@
|
|||||||
|
|
||||||
function attachInteraction(canvas, state) {
|
function attachInteraction(canvas, state) {
|
||||||
canvas.onpointerdown = event => {
|
canvas.onpointerdown = event => {
|
||||||
|
event.preventDefault();
|
||||||
state.dragging = true;
|
state.dragging = true;
|
||||||
|
state.dragMode = event.button === 2 || event.button === 1 || event.shiftKey ? "pan" : "rotate";
|
||||||
state.lastX = event.clientX;
|
state.lastX = event.clientX;
|
||||||
state.lastY = event.clientY;
|
state.lastY = event.clientY;
|
||||||
canvas.setPointerCapture(event.pointerId);
|
canvas.setPointerCapture(event.pointerId);
|
||||||
@@ -115,29 +133,66 @@
|
|||||||
const dy = event.clientY - state.lastY;
|
const dy = event.clientY - state.lastY;
|
||||||
state.lastX = event.clientX;
|
state.lastX = event.clientX;
|
||||||
state.lastY = event.clientY;
|
state.lastY = event.clientY;
|
||||||
|
if (state.dragMode === "pan") {
|
||||||
|
panCamera(state, dx, dy);
|
||||||
|
} else {
|
||||||
state.angleY += dx * 0.008;
|
state.angleY += dx * 0.008;
|
||||||
state.angleX = Math.max(-1.25, Math.min(-0.15, state.angleX + dy * 0.006));
|
state.angleX = Math.max(-1.25, Math.min(-0.15, state.angleX + dy * 0.006));
|
||||||
|
}
|
||||||
renderState(state, canvas);
|
renderState(state, canvas);
|
||||||
};
|
};
|
||||||
canvas.onpointerup = event => {
|
canvas.onpointerup = event => {
|
||||||
state.dragging = false;
|
state.dragging = false;
|
||||||
try { canvas.releasePointerCapture(event.pointerId); } catch { }
|
try { canvas.releasePointerCapture(event.pointerId); } catch { }
|
||||||
};
|
};
|
||||||
|
canvas.onpointercancel = () => {
|
||||||
|
state.dragging = false;
|
||||||
|
};
|
||||||
|
canvas.oncontextmenu = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
canvas.onwheel = event => {
|
canvas.onwheel = event => {
|
||||||
event.preventDefault();
|
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);
|
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) {
|
function renderState(state, canvas) {
|
||||||
const width = canvas.clientWidth || 900;
|
const width = canvas.clientWidth || 900;
|
||||||
const height = canvas.clientHeight || 520;
|
const height = canvas.clientHeight || 520;
|
||||||
state.camera.aspect = width / height;
|
state.camera.aspect = width / height;
|
||||||
state.camera.updateProjectionMatrix();
|
state.camera.updateProjectionMatrix();
|
||||||
const horizontal = Math.cos(state.angleX) * state.distance;
|
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.position.set(
|
||||||
state.camera.lookAt(0, 2.8, 0);
|
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.setSize(width, height, false);
|
||||||
state.renderer.render(state.scene, state.camera);
|
state.renderer.render(state.scene, state.camera);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user