Browse Source

improved graph rendering

master
Youen Toupin 3 years ago
parent
commit
4b3bfa9ac0
  1. 19
      simulator/src/app.scss
  2. 30
      simulator/src/simulator-ui.ts
  3. 8
      simulator/src/simulator.ts
  4. 145
      simulator/src/svg-drawing.ts
  5. 2
      simulator/tools/purify.js

19
simulator/src/app.scss

@ -51,10 +51,27 @@ svg text {
.battery-charge-graph { .battery-charge-graph {
margin-top: 2rem; margin-top: 2rem;
overflow-x: auto;
} }
.battery-charge-graph { .battery-charge-graph svg {
min-height: 300px;
}
.battery-charge-graph line {
stroke: black; stroke: black;
}
.battery-charge-graph .graph {
stroke: #090;
fill: none; fill: none;
stroke-width: 1px; stroke-width: 1px;
} }
.battery-charge-graph .graph.grid-recharge {
stroke: #00c;
}
.battery-charge-graph text {
font-size: 10px;
}

30
simulator/src/simulator-ui.ts

@ -74,8 +74,34 @@ document.addEventListener('DOMContentLoaded', function() {
let resultsContainer = container.querySelector('.simulation-results'); let resultsContainer = container.querySelector('.simulation-results');
let batteryChargeGraph = new SvgDrawing.SvgElement(resultsContainer.querySelector('.battery-charge-graph svg')); let batteryChargeGraph = new SvgDrawing.SvgElement(resultsContainer.querySelector('.battery-charge-graph svg'));
batteryChargeGraph.viewport.setLogical({ x: 0, y: 0, width: 365*24, height: parameters.batteryCapacity });
batteryChargeGraph.graph(simulationResult.batteryLevel); batteryChargeGraph.clear();
let marginTop = 20;
let marginBottom = 20;
let marginLeft = 40;
let marginRight = 20;
batteryChargeGraph.line({ x: marginLeft, y: marginBottom }, { x: batteryChargeGraph.width - marginRight, y: marginBottom });
batteryChargeGraph.line({ x: marginLeft, y: marginBottom }, { x:marginLeft, y: batteryChargeGraph.height - marginTop });
batteryChargeGraph.text({ x: marginLeft-3, y: marginBottom }, '0%', 'end', 0.6);
batteryChargeGraph.text({ x: marginLeft-3, y: batteryChargeGraph.height - marginTop }, '100%', 'end', 0.6);
batteryChargeGraph.viewport.setData({ x: 0, y: 0, width: 365*24, height: parameters.batteryCapacity });
batteryChargeGraph.viewport.setView({ x: marginLeft, y: batteryChargeGraph.height - marginBottom, width: batteryChargeGraph.width - (marginLeft+marginRight), height: -batteryChargeGraph.height+(marginTop+marginBottom) });
batteryChargeGraph.graph(simulationResult.batteryLevel, simulationResult.batteryLevel.map(x => x == 0 ? 1 : 0), [{className: ''}, {className: 'grid-recharge'}]);
let months = ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Jui', 'Jui', 'Aou', 'Sep', 'Oct', 'Nov', 'Dec'];
let monthWidth = 365*24/12
for(let month = 0; month < 12; ++month) {
batteryChargeGraph.text({ x: (month+0.5)*monthWidth, y: 0 }, months[month], 'middle', -0.1);
}
batteryChargeGraph.viewport.setData({ x: 0, y: 0, width: 365*24, height: 1 });
batteryChargeGraph.viewport.setView({ x: marginLeft, y: batteryChargeGraph.height - marginBottom, width: batteryChargeGraph.width - (marginLeft+marginRight), height: -15 });
for(let month = 0; month < 13; ++month) {
batteryChargeGraph.line({ x: month*monthWidth, y: 0 }, { x: month*monthWidth, y: -1 });
}
resultsContainer.classList.toggle('is-hidden', false); resultsContainer.classList.toggle('is-hidden', false);
}); });

8
simulator/src/simulator.ts

@ -92,11 +92,15 @@ namespace Simulator {
// TODO: we should keep a margin because real users will recharge before they reach the bare minimum required for an outing // TODO: we should keep a margin because real users will recharge before they reach the bare minimum required for an outing
remainingBatteryCharge += solarCharge - consumption; remainingBatteryCharge += solarCharge - consumption;
let gridRecharge = false;
if(remainingBatteryCharge > vehicle.batteryCapacity) { if(remainingBatteryCharge > vehicle.batteryCapacity) {
solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity; solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity;
remainingBatteryCharge = vehicle.batteryCapacity; remainingBatteryCharge = vehicle.batteryCapacity;
} }
else if(remainingBatteryCharge <= 0) { else if(remainingBatteryCharge <= 0) {
// TODO: detect if battery capacity is too low for a single outing, abort simulation and display an explanation for the user
gridRecharge = true;
let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge; let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge;
remainingBatteryCharge += rechargeEnergy; remainingBatteryCharge += rechargeEnergy;
result.cumulatedGridRechargeEnergy += rechargeEnergy; result.cumulatedGridRechargeEnergy += rechargeEnergy;
@ -106,8 +110,8 @@ namespace Simulator {
result.cumulatedMotorConsumption += consumption; result.cumulatedMotorConsumption += consumption;
result.cumulatedSolarRechargeEnergy += solarCharge; result.cumulatedSolarRechargeEnergy += solarCharge;
result.batteryLevel[hourIdx] = remainingBatteryCharge; result.batteryLevel[hourIdx] = gridRecharge ? 0 : remainingBatteryCharge;
} }
} }
return result; return result;

145
simulator/src/svg-drawing.ts

@ -40,46 +40,101 @@ namespace SvgDrawing {
} }
export class Viewport { export class Viewport {
private invLogicalW: number = 0; private invDataW: number = 0;
private invLogicalH: number = 0; private invDataH: number = 0;
constructor(private logical: Rect, private view: Rect) { this.update(); } constructor(private data: Rect, private view: Rect) { this.update(); }
setLogical(r: Rect) { this.logical = r; this.update(); } setData(r: Rect) { this.data = r; this.update(); }
setView(r: Rect) { this.view = r; this.update(); }
xLogicalToView(x: number) { return (x - this.logical.x) / this.logical.width * this.view.width + this.view.x; } xDataToView(x: number) { return (x - this.data.x) / this.data.width * this.view.width + this.view.x; }
yLogicalToView(y: number) { return (y - this.logical.y) / this.logical.height * this.view.height + this.view.y; } yDataToView(y: number) { return (y - this.data.y) / this.data.height * this.view.height + this.view.y; }
logicalToView(p: Point, out_point: Point) { dataToView(p: Point, out_point: Point) {
out_point.x = (p.x - this.logical.x) * this.invLogicalW * this.view.width + this.view.x; out_point.x = (p.x - this.data.x) * this.invDataW * this.view.width + this.view.x;
out_point.y = (p.y - this.logical.y) * this.invLogicalH * this.view.height + this.view.y; out_point.y = (p.y - this.data.y) * this.invDataH * this.view.height + this.view.y;
} }
private update() { private update() {
this.invLogicalW = 1.0 / this.logical.width; this.invDataW = 1.0 / this.data.width;
this.invLogicalH = 1.0 / this.logical.height; this.invDataH = 1.0 / this.data.height;
} }
} }
export interface LineStyle {
className: string;
}
export class SvgElement { export class SvgElement {
public viewport: Viewport; public viewport: Viewport;
public width: number;
public height: number;
constructor(private htmlElement: HTMLElement) { constructor(private htmlElement: HTMLElement) {
let viewBox = htmlElement.getAttribute('viewBox').split(' '); let viewBox = htmlElement.getAttribute('viewBox').split(' ');
let r: Rect = { x: Number(viewBox[0]), y: Number(viewBox[1]), width: Number(viewBox[2]), height: Number(viewBox[3]) }; let r: Rect = { x: Number(viewBox[0]), y: Number(viewBox[1]), width: Number(viewBox[2]), height: Number(viewBox[3]) };
this.viewport = new Viewport(r, { x: r.x, y: r.y + r.height, width: r.width, height: -r.height }); this.viewport = new Viewport(r, { x: r.x, y: r.y + r.height, width: r.width, height: -r.height });
this.width = r.width;
this.height = r.height;
}
clear() {
this.htmlElement.querySelectorAll('*').forEach(elt => elt.remove());
}
line(start: Point, end: Point): SVGLineElement {
let line = document.createElementNS('http://www.w3.org/2000/svg','line');
line.setAttribute('x1', this.viewport.xDataToView(start.x).toString());
line.setAttribute('y1', this.viewport.yDataToView(start.y).toString());
line.setAttribute('x2', this.viewport.xDataToView(end.x).toString());
line.setAttribute('y2', this.viewport.yDataToView(end.y).toString());
this.htmlElement.append(line);
return line;
}
text(position: Point, value: string, anchor?: string, verticalAlign?: number): SVGTextElement {
let text = document.createElementNS('http://www.w3.org/2000/svg','text');
text.setAttribute('x', this.viewport.xDataToView(position.x).toString());
text.setAttribute('y', this.viewport.yDataToView(position.y).toString());
if(anchor !== undefined) text.setAttribute('text-anchor', anchor);
if(verticalAlign !== undefined) text.setAttribute('dy', (1.0-verticalAlign) + 'em');
text.appendChild(document.createTextNode(value));
this.htmlElement.append(text);
return text;
} }
graph(y: number[]): SVGPathElement; graph(y: number[]): SVGPathElement[];
graph(x: number[], y: number[]): SVGPathElement; graph(x: number[], y: number[]): SVGPathElement[];
graph(arg1: number[], arg2?: number[]) { graph(y: number[], indices: number[], styles: LineStyle[]): SVGPathElement[];
graph(x: number[], y: number[], indices: number[], styles: LineStyle[]): SVGPathElement[];
graph(arg1: number[], arg2?: number[], arg3?: number[] | LineStyle[], arg4?: LineStyle[]): SVGPathElement[] {
let indices: number[] | null = null;
let styles: LineStyle[] | null = null;
if(arg3) {
if(arg4) {
indices = <number[]>arg3;
styles = arg4;
}
else {
indices = arg2;
styles = <LineStyle[]>arg3;
}
}
if(styles == null) {
styles = [{className: ''}];
}
let read = (idx: number, out_point: Point) => { let read = (idx: number, out_point: Point) => {
out_point.x = arg1[idx]; out_point.x = arg1[idx];
out_point.y = arg2[idx]; out_point.y = arg2[idx];
return true; return true;
}; };
if(!arg2) { let reset = (idx: number) => {};
if(!arg2 || (arg3 && typeof(arg3[0]) !== 'number')) {
read = (idx: number, out_point: Point) => { read = (idx: number, out_point: Point) => {
out_point.x = idx; out_point.x = idx;
out_point.y = arg1[idx]; out_point.y = arg1[idx];
@ -100,7 +155,6 @@ namespace SvgDrawing {
let dp = Math.cos(optimizeCurveAngle/180*Math.PI); let dp = Math.cos(optimizeCurveAngle/180*Math.PI);
let lastDrawnPoint: Point = { x: 0, y: 0 }; let lastDrawnPoint: Point = { x: 0, y: 0 };
read(0, lastDrawnPoint);
let nextPoint: Point = { x: 0, y: 0 }; let nextPoint: Point = { x: 0, y: 0 };
let dir: Point = { x: 0, y: 0 }; let dir: Point = { x: 0, y: 0 };
@ -108,6 +162,12 @@ namespace SvgDrawing {
let nextSegDir: Point = { x: 0, y: 0 }; let nextSegDir: Point = { x: 0, y: 0 };
let perp: Point = { x: 0, y: 0 }; let perp: Point = { x: 0, y: 0 };
let rawReset = reset;
reset = (idx: number) => {
rawReset(idx);
read(idx, lastDrawnPoint);
};
read = (idx: number, out_point: Point) => { read = (idx: number, out_point: Point) => {
rawRead(idx, out_point); rawRead(idx, out_point);
if(idx == 0 || idx == num - 1) return true; if(idx == 0 || idx == num - 1) return true;
@ -140,31 +200,48 @@ namespace SvgDrawing {
let startTime = performance.now(); let startTime = performance.now();
let count = 0; let count = 0;
let logicalPoint: Point = { x: 0, y: 0 }; let paths = [];
let viewPoint: Point = { x: 0, y: 0 };
read(0, logicalPoint);
this.viewport.logicalToView(logicalPoint, viewPoint);
let coordinates = 'M'+Math.round(viewPoint.x)+','+Math.round(viewPoint.y);
coordinates += ' L';
for(let idx = 0; idx < num; ++idx) { for(let styleIdx = 0; styleIdx < styles.length; ++styleIdx) {
if(read(idx, logicalPoint)) { let dataPoint: Point = { x: 0, y: 0 };
this.viewport.logicalToView(logicalPoint, viewPoint); let viewPoint: Point = { x: 0, y: 0 };
coordinates += Math.round(viewPoint.x)+','+Math.round(viewPoint.y)+' ';
count += 1; let coordinates = '';
let open = false;
for(let idx = 0; idx < num; ++idx) {
let includePoint = !indices || indices[idx] == styleIdx;
if(!open && includePoint) {
reset(idx);
read(idx, dataPoint);
this.viewport.dataToView(dataPoint, viewPoint);
let space = coordinates == '' ? '' : ' ';
coordinates += space+'M'+Math.round(viewPoint.x)+','+Math.round(viewPoint.y) + ' L';
count += 1;
}
else if(open && (read(idx, dataPoint) || !includePoint)) {
this.viewport.dataToView(dataPoint, viewPoint);
coordinates += ' '+Math.round(viewPoint.x)+','+Math.round(viewPoint.y);
count += 1;
}
open = includePoint;
} }
let style = styles[styleIdx];
let path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('class', 'graph' + (style.className == '' ? '' : ' ' + style.className));
path.setAttribute('d', coordinates);
this.htmlElement.append(path);
paths.push(path);
} }
let path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('class','graph');
path.setAttribute('d', coordinates);
this.htmlElement.append(path);
let endTime = performance.now(); let endTime = performance.now();
console.log("graph: " + count + " points, " + (endTime - startTime) + "ms"); console.log("graph: " + count + " points, " + (endTime - startTime) + "ms");
return path; return paths;
} }
} }
} }

2
simulator/tools/purify.js

@ -8,7 +8,7 @@ let css = ['../.intermediate/simulator.css'];
let options = { let options = {
output: '../www/simulator.css', output: '../www/simulator.css',
whitelist: ['is-multiple', 'is-loading', 'is-narrow', 'is-active', 'climate-zone', 'is-max-desktop', 'is-max-widescreen'], whitelist: ['is-multiple', 'is-loading', 'is-narrow', 'is-active', 'climate-zone', 'grid-recharge', 'is-max-desktop', 'is-max-widescreen', 'line'],
minify: false, minify: false,
info: false info: false
}; };

Loading…
Cancel
Save