<commit_before>/**
 * Fund graphs
 */

import $ from "../../lib/jquery.min";

import {
  MSG_TIME_ERROR,
  COLOR_GRAPH_FUND_ITEM, COLOR_GRAPH_FUND_POINT,
  COLOR_DARK, COLOR_LIGHT_GREY,
  COLOR_PROFIT, COLOR_LOSS, COLOR_PROFIT_LIGHT, COLOR_LOSS_LIGHT,
  COLOR_GRAPH_FUND_LINE, COLOR_GRAPH_TITLE, COLOR_KEY,
  GRAPH_FUND_ITEM_LINE_WIDTH, GRAPH_FUND_ITEM_TENSION,
  GRAPH_FUND_HISTORY_TENSION, GRAPH_FUND_HISTORY_POINT_RADIUS,
  GRAPH_FUND_HISTORY_NUM_TICKS, GRAPH_FUND_HISTORY_LINE_WIDTH,
  GRAPH_FUND_HISTORY_WIDTH_NARROW, GRAPH_FUND_HISTORY_WIDTH,
  STOCKS_REFRESH_INTERVAL, STOCKS_HL_TIME, DO_STOCKS_LIST, STOCK_INDICES,
  FONT_AXIS_LABEL
} from "const";

import { formatAge, formatCurrency } from "misc/format";
import MediaQueryHandler from "misc/media_query";
import { todayDate, timeSeriesTicks } from "misc/date";

import { getTickSize, LineGraph } from "graph/graph";
import { GoogleFinanceAPI } from "api/api";

const windowSize = new MediaQueryHandler();
const finance = new GoogleFinanceAPI();

const numDp = (stockChange, width) => {
  width = width || 2;
  return stockChange === 0 ? width : Math.max(
      0, width - Math.max(
        0, Math.floor(Math.log(Math.abs(stockChange)) / Math.LN10)
      )
    );
};

const processStockChange = (res, old) => {
  const price = parseFloat(res.l_fix, 10);
  const change = parseFloat(res.c_fix, 10) / price * 100;

  old.delta = old.price !== price ? (old.price < price ? 1 : -1) : 0;
  old.price = price;
  old.change = change;
  old.changeText = (change >= 0 ? "+" : "") + change.toFixed(numDp(change));

  return old;
};

export class GraphFundItem extends LineGraph {
  constructor(options, api) {
    const minX = Math.min.apply(null, options.data.map(item => item[0]));
    const maxX = Math.max.apply(null, options.data.map(item => item[0]));

    const minY = Math.min.apply(null, options.data.map(item => item[1]));
    const maxY = Math.max.apply(null, options.data.map(item => item[1]));

    options.range = [minX, maxX, minY, maxY];

    super(options, api);

    this.dataMinY = minY;
    this.dataMaxY = maxY;

    this.colors = [COLOR_GRAPH_FUND_ITEM];
    this.lineWidth = GRAPH_FUND_ITEM_LINE_WIDTH;
    this.tension = GRAPH_FUND_ITEM_TENSION;

    this.data = options.data;

    this.defaultWidth = this.width;
    this.defaultHeight = this.height;

    this.popout = false;
    this.$canvas.on("click", () => this.togglePopout());
  }

  togglePopout() {
    // make the graph larger
    this.popout = !this.popout;

    this.$canvas.toggleClass("popout", this.popout);

    this.width = this.popout ? this.$canvas.width() : this.defaultWidth;
    this.height = this.popout ? this.$canvas.height() : this.defaultHeight;

    this.$canvas[0].width = this.width;
    this.$canvas[0].height = this.height;

    const minY = this.popout ? Math.floor(this.dataMinY) : this.dataMinY;
    const maxY = this.popout ? Math.ceil(this.dataMaxY) : this.dataMaxY;

    this.setRange([this.minX, this.maxX, minY, maxY]);

    this.draw();
  }

  draw() {
    if (!this.supported) {
      return;
    }

    // clear canvas
    this.ctx.clearRect(0, 0, this.width, this.height);

    // draw axes
    this.ctx.lineWidth = 1;

    if (this.popout || (this.maxY - this.minY) < this.height / 2) {
      this.ctx.fillStyle = COLOR_DARK;
      this.ctx.textBaseline = "middle";
      this.ctx.textAlign = "left";
      this.ctx.font = FONT_AXIS_LABEL;

      const tickPad = this.popout ? 40 : 0;

      const range = this.maxY - this.minY;
      const inc = this.popout
        ? Math.round(
          Math.max(20, this.height / range) / this.height * range / 2
        ) * 2
        : 1;

      for (let i = Math.floor(this.minY / inc) * inc; i <= this.maxY; i += inc) {
        const tickPos = Math.floor(this.pixY(i)) + 0.5;

        if (this.popout) {
          const tickName = i.toFixed(1) + "%";
          this.ctx.fillText(tickName, this.padX1, tickPos);
        }

        this.ctx.beginPath();
        this.ctx.moveTo(this.padX1 + tickPad, tickPos);
        this.ctx.lineTo(this.width - this.padX2, tickPos);
        this.ctx.closePath();
        this.ctx.strokeStyle = i <= 0 ? COLOR_LOSS_LIGHT : COLOR_PROFIT_LIGHT;
        this.ctx.stroke();
      }
    }

    // plot data
    this.drawCubicLine(this.data, this.colors);
  }
}

export class GraphFundHistory extends LineGraph {
  constructor(options, api, state) {
    super(options, api, state);

    this.$list = options.$list;

    this.tension = GRAPH_FUND_HISTORY_TENSION;
    this.raw = options.data;
    this.funds = options.funds;
    this.fundLines = this.funds.map(() => false);
    this.startTime = options.startTime;
    this.dataOffset = 0;
    this.hlPoint = -1;

    this.togglePercent(true, true);

    this.colorMajor = COLOR_GRAPH_FUND_LINE;

    this.$label = $("<div></div>").addClass("label");
    this.$gCont.append(this.$fundSidebar);
    this.buildFundSidebar();
    this.$gCont.append(this.$label);
<commit_msg>Profit/loss colours on fund item graph line<commit_after>/**
 * Fund graphs
 */

import $ from "../../lib/jquery.min";

import {
  MSG_TIME_ERROR,
  COLOR_GRAPH_FUND_POINT, COLOR_DARK, COLOR_LIGHT_GREY,
  COLOR_PROFIT, COLOR_LOSS,
  COLOR_GRAPH_FUND_LINE, COLOR_GRAPH_TITLE, COLOR_KEY,
  GRAPH_FUND_ITEM_LINE_WIDTH, GRAPH_FUND_ITEM_TENSION,
  GRAPH_FUND_HISTORY_TENSION, GRAPH_FUND_HISTORY_POINT_RADIUS,
  GRAPH_FUND_HISTORY_NUM_TICKS, GRAPH_FUND_HISTORY_LINE_WIDTH,
  GRAPH_FUND_HISTORY_WIDTH_NARROW, GRAPH_FUND_HISTORY_WIDTH,
  STOCKS_REFRESH_INTERVAL, STOCKS_HL_TIME, DO_STOCKS_LIST, STOCK_INDICES,
  FONT_AXIS_LABEL
} from "const";

import { formatAge, formatCurrency } from "misc/format";
import MediaQueryHandler from "misc/media_query";
import { todayDate, timeSeriesTicks } from "misc/date";

import { getTickSize, LineGraph } from "graph/graph";
import { GoogleFinanceAPI } from "api/api";

const windowSize = new MediaQueryHandler();
const finance = new GoogleFinanceAPI();

const numDp = (stockChange, width) => {
  width = width || 2;
  return stockChange === 0 ? width : Math.max(
      0, width - Math.max(
        0, Math.floor(Math.log(Math.abs(stockChange)) / Math.LN10)
      )
    );
};

const processStockChange = (res, old) => {
  const price = parseFloat(res.l_fix, 10);
  const change = parseFloat(res.c_fix, 10) / price * 100;

  old.delta = old.price !== price ? (old.price < price ? 1 : -1) : 0;
  old.price = price;
  old.change = change;
  old.changeText = (change >= 0 ? "+" : "") + change.toFixed(numDp(change));

  return old;
};

export class GraphFundItem extends LineGraph {
  constructor(options, api) {
    const minX = Math.min.apply(null, options.data.map(item => item[0]));
    const maxX = Math.max.apply(null, options.data.map(item => item[0]));

    const minY = Math.min.apply(null, options.data.map(item => item[1]));
    const maxY = Math.max.apply(null, options.data.map(item => item[1]));

    options.range = [minX, maxX, minY, maxY];

    super(options, api);

    this.dataMinY = minY;
    this.dataMaxY = maxY;

    this.lineWidth = GRAPH_FUND_ITEM_LINE_WIDTH;
    this.tension = GRAPH_FUND_ITEM_TENSION;

    this.data = options.data;

    this.genColors();

    this.defaultWidth = this.width;
    this.defaultHeight = this.height;

    this.popout = false;
    this.$canvas.on("click", () => this.togglePopout());
  }
  genColors() {
    const colors = [];
    const transition = [];
    const levels = [
      [0, COLOR_LOSS],
      [Infinity, COLOR_PROFIT]
    ];

    if (this.data.length > 1) {
      let level = levels.findIndex(item => this.data[1][1] < item[0]);
      colors.push(levels[level][1]);

      this.data.slice(2).forEach((point, key) => {
        const thisLevel = levels.findIndex(item => point[1] < item[0]);

        if (thisLevel !== level) {
          level = thisLevel;
          colors.push(levels[level][1]);
          transition.push(key);
        }
      });

      this.colors = colors;
      this.transition = transition;
    }
    else {
      this.colors = [COLOR_DARK];
    }
  }
  togglePopout() {
    // make the graph larger
    this.popout = !this.popout;

    this.$canvas.toggleClass("popout", this.popout);

    this.width = this.popout ? this.$canvas.width() : this.defaultWidth;
    this.height = this.popout ? this.$canvas.height() : this.defaultHeight;

    this.$canvas[0].width = this.width;
    this.$canvas[0].height = this.height;

    const minY = this.popout ? Math.floor(this.dataMinY) : this.dataMinY;
    const maxY = this.popout ? Math.ceil(this.dataMaxY) : this.dataMaxY;

    this.setRange([this.minX, this.maxX, minY, maxY]);

    this.draw();
  }

  draw() {
    if (!this.supported) {
      return;
    }

    // clear canvas
    this.ctx.clearRect(0, 0, this.width, this.height);

    // draw axes
    this.ctx.lineWidth = 1;

    if (this.popout) {
      this.ctx.fillStyle = COLOR_DARK;
      this.ctx.textBaseline = "middle";
      this.ctx.textAlign = "left";
      this.ctx.font = FONT_AXIS_LABEL;

      const range = this.maxY - this.minY;
      const inc = this.popout
        ? Math.round(
          Math.max(20, this.height / range) / this.height * range / 2
        ) * 2
        : 1;

      for (let i = Math.floor(this.minY / inc) * inc; i <= this.maxY; i += inc) {
        const tickPos = Math.floor(this.pixY(i)) + 0.5;
        const tickName = i.toFixed(1) + "%";
        this.ctx.fillText(tickName, this.padX1, tickPos);
      }
    }

    // plot data
    this.drawCubicLine(this.data, this.colors);
  }
}

export class GraphFundHistory extends LineGraph {
  constructor(options, api, state) {
    super(options, api, state);

    this.$list = options.$list;

    this.tension = GRAPH_FUND_HISTORY_TENSION;
    this.raw = options.data;
    this.funds = options.funds;
    this.fundLines = this.funds.map(() => false);
    this.startTime = options.startTime;
    this.dataOffset = 0;
    this.hlPoint = -1;

    this.togglePercent(true, true);

    this.colorMajor = COLOR_GRAPH_FUND_LINE;

    this.$label = $("<div></div>").addClass("label");
    this.$gCont.append(this.$fundSidebar);
    this.buildFundSidebar();
    this.$gCont.append(this.$label);

{'fraction_non_alphanumeric': 0.14418054597417956, 'fraction_numerical': 0.008968167931408299, 'mean_word_length': 3.0592, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5491676', 'n_tokens_mistral': 3821, 'n_tokens_neox': 3636, 'n_words': 680}