Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ This repo is still in progress.
- [x] Display table with selected data
- [x] Mouse tooltip
- [ ] New measurement: average repair fees
- [x] Improve color map
- [ ] Use N/A to represent missing data rather than 0
- [x] Trend graph
- [ ] Mouse tooltip
- [ ] Dark mode
Expand Down
2 changes: 1 addition & 1 deletion src/app/link/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export abstract class Link extends NavTab {
.append("a")
.attr("id", this.id)
.attr("href", this.url)
.attr("rel", "noopener noreferrer")
.attr("rel", "noopener")
.attr("target", "_blank")
.classed("link-tab", true)
.html(this.name)
Expand Down
44 changes: 44 additions & 0 deletions src/misc/color-map-def.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CONT_COLORS, Container } from "../utils";

export class ColorMap {
constructor(
public name: string,
public colors: Array<string>,
public percentiles: Array<number>
) { }
}


export class BrHeatColorMap {
win_rate: { Aviation: ColorMap; Ground_vehicles: ColorMap };
battles_sum: { Aviation: ColorMap; Ground_vehicles: ColorMap };
}

const brHeatmapColorMaps: BrHeatColorMap = {
"win_rate": {
"Ground_vehicles": new ColorMap(
"ground-vehicles-win-rate",
[CONT_COLORS.BLACK, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK],
[0, 0.05, 0.43, 0.53, 0.63, 0.95, 1.0]
),
"Aviation": new ColorMap(
"aviation-win-rate",
[CONT_COLORS.BLACK, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK],
[0, 0.01, 0.5, 0.6, 0.7, 0.99, 1.0]
)
},
"battles_sum": {
"Ground_vehicles": new ColorMap(
"ground-vehicles-battles-sum",
[CONT_COLORS.BLACK, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK],
[0, 0.01, 0.4, 0.5, 0.6, 0.99, 1.0],
),
"Aviation": new ColorMap(
"aviation-battles-sum",
[CONT_COLORS.BLACK, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK],
[0, 0.01, 0.4, 0.5, 0.6, 0.99, 1.0],
)
}
}

Container.bind(BrHeatColorMap).toConstantValue(brHeatmapColorMaps);
109 changes: 49 additions & 60 deletions src/plot/br-heatmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Config, Localization, Margin, MeasurementTranslator, NationTranslator }
import { brs, Content, dataUrl, nations } from "../app/global-env";
import { BRHeatMapPage } from "../app/page/br-heatmap-page";
import { Nation } from "../data/wiki-data";
import { BrHeatColorMap } from "../misc/color-map-def";


@Provider(BrHeatmap)
Expand All @@ -27,6 +28,7 @@ export class BrHeatmap extends Plot {
@Inject(BrHeatmapTooltip) readonly tooltip: Tooltip;
@Inject(Content) readonly content: d3.Selection<HTMLDivElement, unknown, HTMLElement, any>
@Inject(BRHeatMapPage) readonly page: BRHeatMapPage;
@Inject(BrHeatColorMap) readonly colorMaps: BrHeatColorMap;

selected: Array<SquareInfo> = [];

Expand All @@ -42,60 +44,47 @@ export class BrHeatmap extends Plot {
await this.legend.update();
}

get mouseleaveEvent(): () => void {
const self = this;
return function(): void {
d3.select(this).style("stroke", "black");
self.tooltip.hide();
};
onPointerLeave(_: SquareInfo, node: SVGRectElement): void {
d3.select(node).style("stroke", "black");
this.tooltip.hide();
}

get mouseoverEvent(): (d: SquareInfo) => void {
const self = this;
return function(d: SquareInfo): void {
d3.select(this).style("stroke", "white");
self.tooltip.appear();
self.tooltip.rect
.transition()
.duration(100)
.style("fill", self.value2color(d.value));
}
onPointerOver(d: SquareInfo, node: SVGRectElement): void {
d3.select(node).style("stroke", "white");
this.tooltip.appear();
this.tooltip.rect
.transition()
.duration(100)
.style("fill", this.value2color(d.value));
}

get mousemoveEvent(): (d: SquareInfo) => void {
const self = this;
return async function(d: SquareInfo): Promise<void> {
const mousePos = new MousePosition(
d3.mouse(this)[0],
d3.mouse(this)[1]
);
await self.tooltip.update([
`${Container.get(Localization.BrHeatmapPage.Tooltip.nation)}${Container.get<NationTranslator>(Localization.Nation)(d.nation)}`,
`${Container.get(Localization.BrHeatmapPage.Tooltip.br)}${d.br}`,
`${Container.get<MeasurementTranslator>(Localization.Measurement)(self.page.measurement)}: ${_.round(d.value, 3)}`
], mousePos);
}
async onPointerMove(d: SquareInfo, node: SVGRectElement): Promise<void> {
await this.tooltip.update([
`${Container.get(Localization.BrHeatmapPage.Tooltip.nation)}${Container.get<NationTranslator>(Localization.Nation)(d.nation)}`,
`${Container.get(Localization.BrHeatmapPage.Tooltip.br)}${d.br}`,
`${Container.get<MeasurementTranslator>(Localization.Measurement)(this.page.measurement)}: ${_.round(d.value, 3)}`
], new MousePosition(
d3.mouse(node)[0],
d3.mouse(node)[1]
));
}

get clickEvent(): () => void {
const self = this;
return async function(): Promise<void> {
const square: d3.Selection<SVGRectElement, SquareInfo, HTMLElement, any> = d3.select(this);
const info: SquareInfo = square.data()[0];

if (utils.rgbToHex(square.style("fill")).toUpperCase() === COLORS.AZURE) {
// if the square is selected
square.style("fill", self.value2color(info.value));
// remove the item in the `this.selected`
self.selected = self.selected.filter(each => each.br !== info.br || each.nation !== info.nation);
} else {
// if the square is not selected
square.style("fill", COLORS.AZURE);
// add the item into the `this.selected`
self.selected.push(info);
}
await self.updateSubPlots()
async onClick(_: SquareInfo, node: SVGRectElement): Promise<void> {
const square: d3.Selection<SVGRectElement, SquareInfo, HTMLElement, any> = d3.select(node);
const info: SquareInfo = square.data()[0];

if (utils.rgbToHex(square.style("fill")).toUpperCase() === COLORS.AZURE) {
// if the square is selected
square.style("fill", this.value2color(info.value));
// remove the item in the `this.selected`
this.selected = this.selected.filter(each => each.br !== info.br || each.nation !== info.nation);
} else {
// if the square is not selected
square.style("fill", COLORS.AZURE);
// add the item into the `this.selected`
this.selected.push(info);
}
await this.updateSubPlots();
}

cache: TimeseriesData;
Expand Down Expand Up @@ -174,10 +163,10 @@ export class BrHeatmap extends Plot {
.style("fill", d => this.value2color(d.value))
.style("stroke-width", 1)
.style("stroke", "black")
.on("mouseover", this.mouseoverEvent)
.on("mouseleave", this.mouseleaveEvent)
.on("mousemove", this.mousemoveEvent)
.on("click", this.clickEvent);
.on("pointerover", utils.eventWrapper<SVGRectElement, typeof this.onPointerOver>(this, this.onPointerOver))
.on("pointerleave", utils.eventWrapper<SVGRectElement, typeof this.onPointerLeave>(this, this.onPointerLeave))
.on("pointermove", utils.eventWrapper<SVGRectElement, typeof this.onPointerMove>(this, this.onPointerMove))
.on("click", utils.eventWrapper<SVGRectElement, typeof this.onClick>(this, this.onClick));

this.cache = data;

Expand Down Expand Up @@ -312,14 +301,14 @@ export class BrHeatmap extends Plot {

if (this.page.clazz === "Ground_vehicles") {
range2color = d3.scaleLinear<string, string>()
.domain([0, 0.05, 0.4, 0.5, 0.6, 0.95, 1.0])
.range([CONT_COLORS.WHITE, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK])
.interpolate(d3.interpolateHcl)
.domain(this.colorMaps.win_rate.Ground_vehicles.percentiles)
.range(this.colorMaps.win_rate.Ground_vehicles.colors)
.interpolate(d3.interpolateHsl)
} else if (this.page.clazz === "Aviation") {
range2color = d3.scaleLinear<string, string>()
.domain([0, 0.01, 0.5, 0.6, 0.7, 0.99, 1.0])
.range([CONT_COLORS.WHITE, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK])
.interpolate(d3.interpolateHcl)
.domain(this.colorMaps.win_rate.Aviation.percentiles)
.range(this.colorMaps.win_rate.Aviation.colors)
.interpolate(d3.interpolateHsl)
}
break;
case "battles_sum":
Expand All @@ -331,9 +320,9 @@ export class BrHeatmap extends Plot {
.range([0, 1]);

range2color = d3.scaleLinear<string, string>()
.domain([0, 0.01, 0.4, 0.5, 0.6, 0.99, 1.0])
.range([CONT_COLORS.WHITE, CONT_COLORS.BLACK, CONT_COLORS.RED, CONT_COLORS.YELLOW, CONT_COLORS.GREEN, CONT_COLORS.BLACK, CONT_COLORS.BLACK])
.interpolate(d3.interpolateHcl)
.domain(this.colorMaps.battles_sum.Ground_vehicles.percentiles)
.range(this.colorMaps.battles_sum.Ground_vehicles.colors)
.interpolate(d3.interpolateHsl)
break;
}

Expand Down
53 changes: 22 additions & 31 deletions src/plot/line-chart.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as d3 from "d3";
import * as _ from "lodash";
import { Plot } from "./plot";
import { BrHeatmap } from "./br-heatmap";
import { BrHeatmap, SquareInfo } from "./br-heatmap";
import { TimeseriesData, TimeseriesRow, TimeseriesRowGetter } from "../data/timeseries-data";
import { Container, Inject, Injectable, MousePosition, nationColors, Provider, utils, WasmUtils } from "../utils";
import { Clazz, Mode, Scale } from "../app/options";
Expand Down Expand Up @@ -44,46 +44,37 @@ export class BrLineChart extends LineChart {
xAxis: d3.ScaleTime<number, number>;
readonly allDates = Application.dates.map(utils.parseDate).map(date => date.getTime()).reverse();

get mouseleaveEvent(): () => void {
const self = this;
return function(): void {
self.tooltip.hide();
};
onPointerLeave(_: SquareInfo, _2: SVGSVGElement): void {
this.tooltip.hide();
}

get mouseoverEvent(): () => void {
const self = this;
return function(): void {
if (self.selected.length > 0) {
self.tooltip.appear();
} else {
self.tooltip.hide();
}
onPointerOver(_: SquareInfo, _2: SVGSVGElement): void {
if (this.selected.length > 0) {
this.tooltip.appear();
} else {
this.tooltip.hide();
}
}

get mousemoveEvent(): () => void {
const self = this;
return async function(): Promise<void> {
const mousePos = new MousePosition(
d3.mouse(this)[0],
d3.mouse(this)[1]
);
const xValue = self.xAxis.invert(mousePos.x - self.margin.left);
self.selectedDate = utils.findClosest(self.allDates, xValue.getTime());
await self.tooltip.update(self.selected.map(dataObj => {
return `${Container.get<NationTranslator>(Localization.Nation)(dataObj.nation)} ` +
`${dataObj.br}: ${_.round(dataObj.values.find(value => value.date.getTime() === self.selectedDate)?.value, 3)}`
}), mousePos);
}
async onPointerMove(_d: SquareInfo, node: SVGSVGElement): Promise<void> {
const mousePos = new MousePosition(
d3.mouse(node)[0],
d3.mouse(node)[1]
);
const xValue = this.xAxis.invert(mousePos.x - this.margin.left);
this.selectedDate = utils.findClosest(this.allDates, xValue.getTime());
await this.tooltip.update(this.selected.map(dataObj =>
`${Container.get<NationTranslator>(Localization.Nation)(dataObj.nation)} ` +
`${dataObj.br}: ${_.round(dataObj.values.find(value => value.date.getTime() === this.selectedDate)?.value, 3)}`
), mousePos);
}

init(): LineChart {
super.init();
this.tooltip.init();
this.svg.on("mousemove", this.mousemoveEvent);
this.svg.on("mouseleave", this.mouseleaveEvent);
this.svg.on("mouseover", this.mouseoverEvent);
this.svg.on("pointerover", utils.eventWrapper<SVGSVGElement, typeof this.onPointerOver>(this, this.onPointerOver));
this.svg.on("pointermove", utils.eventWrapper<SVGSVGElement, typeof this.onPointerMove>(this, this.onPointerMove));
this.svg.on("pointerleave", utils.eventWrapper<SVGSVGElement, typeof this.onPointerLeave>(this, this.onPointerLeave));
return this;
}

Expand Down
15 changes: 12 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export namespace utils {
}
return array[mid];
}

export function eventWrapper<S extends SVGRectElement | SVGSVGElement, T extends (d: SquareInfo, node: S)
=> void | Promise<void>>(thisBinding: unknown, cb: T): (this: S, d: SquareInfo, i: number, n: S[]) => void {
return (d, i, n) =>
// https://stackoverflow.com/questions/27746304/how-to-check-if-an-object-is-a-promise/27760489#27760489
Promise.resolve((cb.bind(thisBinding) as T)(d, n[i])).then(() => {});
}
}

export enum COLORS {
Expand Down Expand Up @@ -176,7 +183,7 @@ export enum CONT_COLORS {
YELLOW = "#FFC425",
GREEN = "#00b159",
BLACK = "#2B2B2B",
WHITE = "#EEFFFF"
WHITE = "#FFFFFF"
}

export class MousePosition {
Expand Down Expand Up @@ -294,8 +301,10 @@ export class ObjChainMap {
}

export class WasmUtils {
static wasm: { extract_data(data_cls: Uint8Array, data_nation: Uint8Array, data_br: Float32Array,
selected_nation: Uint8Array, selected_br: Float32Array, clazz: number): Uint32Array; };
static wasm: {
extract_data(data_cls: Uint8Array, data_nation: Uint8Array, data_br: Float32Array,
selected_nation: Uint8Array, selected_br: Float32Array, clazz: number): Uint32Array;
};

static async init() {
WasmUtils.wasm = await import("wasm-utils");
Expand Down