UNPKG

billboard.js

Version:

Re-usable easy interface JavaScript chart library, based on D3 v4+

257 lines (217 loc) 6.5 kB
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ import {select as d3Select} from "d3-selection"; import {$CANDLESTICK, $COMMON} from "../../config/classes"; import {getRandom, isArray, isNumber, isObject} from "../../module/util"; import type {IOffset} from "./shape"; interface ICandlestickData { open: number; high: number; low: number; close: number; volume?: number; } export default { initCandlestick(): void { const {$el} = this; $el.candlestick = $el.main.select(`.${$COMMON.chart}`) // should positioned at the beginning of the shape node to not overlap others .append("g") .attr("class", $CANDLESTICK.chartCandlesticks); }, /** * Update targets by its data * called from: ChartInternal.updateTargets() * @param {Array} targets Filtered target by type * @private */ updateTargetsForCandlestick(targets): void { const $$ = this; const {$el} = $$; const classChart = $$.getChartClass("Candlestick"); if (!$el.candlestick) { $$.initCandlestick(); } const mainUpdate = $$.$el.main.select(`.${$CANDLESTICK.chartCandlesticks}`) .selectAll(`.${$CANDLESTICK.chartCandlestick}`) .data($$.filterNullish(targets)); mainUpdate.enter().append("g") .attr("class", classChart) .style("pointer-events", "none"); }, /** * Generate/Update elements * @param {boolean} withTransition Transition for exit elements * @param {boolean} isSub Subchart draw * @private */ updateCandlestick(withTransition: boolean, isSub = false): void { const $$ = this; const {$el, $T} = $$; const $root = isSub ? $el.subchart : $el; const classSetter = $$.getClass("candlestick", true); const initialOpacity = $$.initialOpacity.bind($$); const candlestick = $root.main.selectAll(`.${$CANDLESTICK.chartCandlestick}`) .selectAll(`.${$CANDLESTICK.candlestick}`) .data($$.labelishData.bind($$)); $T(candlestick.exit(), withTransition) .style("opacity", "0") .remove(); const candlestickEnter = candlestick.enter() .filter(d => d.value) .append("g") .attr("class", classSetter); candlestickEnter.append("line"); candlestickEnter.append("path"); $root.candlestick = candlestick.merge(candlestickEnter) .style("opacity", initialOpacity); }, /** * Get draw function * @param {object} indices Indice data * @param {boolean} isSub Subchart draw * @returns {Function} * @private */ generateDrawCandlestick(indices, isSub) { const $$ = this; const {config} = $$; const getPoints = $$.generateGetCandlestickPoints(indices, isSub); const isRotated = config.axis_rotated; const downColor = config.candlestick_color_down; return (d, i, g) => { const points = getPoints(d, i); const value = $$.getCandlestickData(d); const isUp = value?._isUp; // switch points if axis is rotated, not applicable for sub chart const indexX = +isRotated; const indexY = +!indexX; if (g.classed) { g.classed($CANDLESTICK[isUp ? "valueUp" : "valueDown"], true); } const path = isRotated ? `H${points[1][1]} V${points[1][0]} H${points[0][1]}` : `V${points[1][1]} H${points[1][0]} V${points[0][1]}`; g.select("path") .attr("d", `M${points[0][indexX]},${points[0][indexY]}${path}z`) .style("fill", d => { const color = isUp ? $$.color(d) : ( isObject(downColor) ? downColor[d.id] : downColor ); return color || $$.color(d); }); // set line position const line = g.select("line"); const pos = isRotated ? { x1: points[2][1], x2: points[2][2], y1: points[2][0], y2: points[2][0] } : { x1: points[2][0], x2: points[2][0], y1: points[2][1], y2: points[2][2] }; for (const x in pos) { line.attr(x, pos[x]); } }; }, /** * Generate shape drawing points * @param {object} indices Indice data * @param {boolean} isSub Subchart draw * @returns {Function} */ generateGetCandlestickPoints(indices, isSub = false): (d, i) => number[][] { const $$ = this; const axis = isSub ? $$.axis.subX : $$.axis.x; const targetsNum = $$.getIndicesMax(indices) + 1; const barW: IOffset = $$.getBarW("candlestick", axis, targetsNum); const x = $$.getShapeX(barW, indices, !!isSub); const y = $$.getShapeY(!!isSub); const shapeOffset = $$.getShapeOffset($$.isBarType, indices, !!isSub); const yScale = $$.getYScaleById.bind($$); return (d, i) => { const y0 = yScale.call($$, d.id, isSub)($$.getShapeYMin(d.id)); const offset = shapeOffset(d, i) || y0; // offset is for stacked bar chart const width = isNumber(barW) ? barW : barW[d.id] || barW._$width; const value = $$.getCandlestickData(d); let points; if (value && isNumber(value.open) && isNumber(value.close)) { const posX = { start: x(d), end: 0 }; posX.end = posX.start + width; const posY = { start: y(value.open), end: y(value.close) }; const posLine = { x: posX.start + (width / 2), high: y(value.high), low: y(value.low) }; posY.start -= y0 - offset; points = [ [posX.start, posY.start], [posX.end, posY.end], [posLine.x, posLine.low, posLine.high] ]; } else { points = [[0, 0], [0, 0], [0, 0, 0]]; } return points; }; }, /** * Redraw function * @param {Function} drawFn Retuned functino from .generateDrawCandlestick() * @param {boolean} withTransition With or without transition * @param {boolean} isSub Subchart draw * @returns {Array} */ redrawCandlestick(drawFn, withTransition?: boolean, isSub = false) { const $$ = this; const {$el, $T} = $$; const {candlestick} = isSub ? $el.subchart : $el; const rand = getRandom(true); return [ candlestick .each(function(d, i) { const g = $T(d3Select(this), withTransition, rand); drawFn(d, i, g); }) .style("opacity", null) ]; }, /** * Get candlestick data as object * @param {object} param Data object * @param {Array|object} param.value Data value * @returns {object|null} Converted data object * @private */ getCandlestickData({value}): ICandlestickData | null { let d; if (isArray(value)) { const [open, high, low, close, volume = false] = value; d = {open, high, low, close}; if (volume !== false) { d.volume = volume; } } else if (isObject(value)) { d = {...value}; } if (d) { d._isUp = d.close >= d.open; } return d || null; } };