improved graph rendering
This commit is contained in:
parent
059dbee24d
commit
4b3bfa9ac0
@ -51,10 +51,27 @@ svg text {
|
||||
|
||||
.battery-charge-graph {
|
||||
margin-top: 2rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.battery-charge-graph {
|
||||
.battery-charge-graph svg {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.battery-charge-graph line {
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.battery-charge-graph .graph {
|
||||
stroke: #090;
|
||||
fill: none;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.battery-charge-graph .graph.grid-recharge {
|
||||
stroke: #00c;
|
||||
}
|
||||
|
||||
.battery-charge-graph text {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
@ -74,8 +74,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let resultsContainer = container.querySelector('.simulation-results');
|
||||
|
||||
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);
|
||||
});
|
||||
|
@ -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
|
||||
remainingBatteryCharge += solarCharge - consumption;
|
||||
|
||||
let gridRecharge = false;
|
||||
if(remainingBatteryCharge > vehicle.batteryCapacity) {
|
||||
solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity;
|
||||
remainingBatteryCharge = vehicle.batteryCapacity;
|
||||
}
|
||||
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;
|
||||
remainingBatteryCharge += rechargeEnergy;
|
||||
result.cumulatedGridRechargeEnergy += rechargeEnergy;
|
||||
@ -106,8 +110,8 @@ namespace Simulator {
|
||||
result.cumulatedMotorConsumption += consumption;
|
||||
result.cumulatedSolarRechargeEnergy += solarCharge;
|
||||
|
||||
result.batteryLevel[hourIdx] = remainingBatteryCharge;
|
||||
}
|
||||
result.batteryLevel[hourIdx] = gridRecharge ? 0 : remainingBatteryCharge;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -40,46 +40,101 @@ namespace SvgDrawing {
|
||||
}
|
||||
|
||||
export class Viewport {
|
||||
private invLogicalW: number = 0;
|
||||
private invLogicalH: number = 0;
|
||||
private invDataW: 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; }
|
||||
yLogicalToView(y: number) { return (y - this.logical.y) / this.logical.height * this.view.height + this.view.y; }
|
||||
xDataToView(x: number) { return (x - this.data.x) / this.data.width * this.view.width + this.view.x; }
|
||||
yDataToView(y: number) { return (y - this.data.y) / this.data.height * this.view.height + this.view.y; }
|
||||
|
||||
logicalToView(p: Point, out_point: Point) {
|
||||
out_point.x = (p.x - this.logical.x) * this.invLogicalW * this.view.width + this.view.x;
|
||||
out_point.y = (p.y - this.logical.y) * this.invLogicalH * this.view.height + this.view.y;
|
||||
dataToView(p: Point, out_point: Point) {
|
||||
out_point.x = (p.x - this.data.x) * this.invDataW * this.view.width + this.view.x;
|
||||
out_point.y = (p.y - this.data.y) * this.invDataH * this.view.height + this.view.y;
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.invLogicalW = 1.0 / this.logical.width;
|
||||
this.invLogicalH = 1.0 / this.logical.height;
|
||||
this.invDataW = 1.0 / this.data.width;
|
||||
this.invDataH = 1.0 / this.data.height;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LineStyle {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export class SvgElement {
|
||||
public viewport: Viewport;
|
||||
public width: number;
|
||||
public height: number;
|
||||
|
||||
constructor(private htmlElement: HTMLElement) {
|
||||
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]) };
|
||||
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;
|
||||
}
|
||||
|
||||
graph(y: number[]): SVGPathElement;
|
||||
graph(x: number[], y: number[]): SVGPathElement;
|
||||
graph(arg1: number[], arg2?: number[]) {
|
||||
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(x: number[], y: number[]): SVGPathElement[];
|
||||
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) => {
|
||||
out_point.x = arg1[idx];
|
||||
out_point.y = arg2[idx];
|
||||
return true;
|
||||
};
|
||||
|
||||
if(!arg2) {
|
||||
let reset = (idx: number) => {};
|
||||
|
||||
if(!arg2 || (arg3 && typeof(arg3[0]) !== 'number')) {
|
||||
read = (idx: number, out_point: Point) => {
|
||||
out_point.x = idx;
|
||||
out_point.y = arg1[idx];
|
||||
@ -100,7 +155,6 @@ namespace SvgDrawing {
|
||||
let dp = Math.cos(optimizeCurveAngle/180*Math.PI);
|
||||
|
||||
let lastDrawnPoint: Point = { x: 0, y: 0 };
|
||||
read(0, lastDrawnPoint);
|
||||
|
||||
let nextPoint: 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 perp: Point = { x: 0, y: 0 };
|
||||
|
||||
let rawReset = reset;
|
||||
reset = (idx: number) => {
|
||||
rawReset(idx);
|
||||
read(idx, lastDrawnPoint);
|
||||
};
|
||||
|
||||
read = (idx: number, out_point: Point) => {
|
||||
rawRead(idx, out_point);
|
||||
if(idx == 0 || idx == num - 1) return true;
|
||||
@ -140,31 +200,48 @@ namespace SvgDrawing {
|
||||
let startTime = performance.now();
|
||||
let count = 0;
|
||||
|
||||
let logicalPoint: Point = { x: 0, y: 0 };
|
||||
let viewPoint: Point = { x: 0, y: 0 };
|
||||
read(0, logicalPoint);
|
||||
this.viewport.logicalToView(logicalPoint, viewPoint);
|
||||
let paths = [];
|
||||
|
||||
let coordinates = 'M'+Math.round(viewPoint.x)+','+Math.round(viewPoint.y);
|
||||
coordinates += ' L';
|
||||
|
||||
for(let idx = 0; idx < num; ++idx) {
|
||||
if(read(idx, logicalPoint)) {
|
||||
this.viewport.logicalToView(logicalPoint, viewPoint);
|
||||
coordinates += Math.round(viewPoint.x)+','+Math.round(viewPoint.y)+' ';
|
||||
count += 1;
|
||||
for(let styleIdx = 0; styleIdx < styles.length; ++styleIdx) {
|
||||
let dataPoint: Point = { x: 0, y: 0 };
|
||||
let viewPoint: Point = { x: 0, y: 0 };
|
||||
|
||||
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();
|
||||
console.log("graph: " + count + " points, " + (endTime - startTime) + "ms");
|
||||
|
||||
return path;
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ let css = ['../.intermediate/simulator.css'];
|
||||
|
||||
let options = {
|
||||
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,
|
||||
info: false
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user