UNPKG

billboard.js

Version:

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

861 lines (858 loc) 32.6 kB
/*! * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license * * billboard.js, JavaScript chart library * https://naver.github.io/billboard.js/ * * @version 4.0.1 */ import { select } from 'd3-selection'; import { curveStepBefore, curveStepAfter, curveStep, curveLinear, curveLinearClosed, curveNatural, curveMonotoneY, curveMonotoneX, curveCatmullRomOpen, curveCatmullRomClosed, curveCatmullRom, curveCardinalOpen, curveCardinalClosed, curveCardinal, curveBundle, curveBasisOpen, curveBasisClosed, curveBasis } from 'd3-shape'; import CLASS from '../../config/classes.js'; import { KEY } from '../../module/Cache.js'; import { getPointer, getRectSegList } from '../../module/util/dom.js'; import { isValue, isObjectType, isFunction, isNumber, notEmpty, isUndefined } from '../../module/util/type-checks.js'; import { parseDate, getUnique, capitalize } from '../../module/util/object.js'; /** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ // Module-level constant: avoids re-creating the lookup object on every getInterpolate() call const CURVE_MAP = { basis: curveBasis, "basis-closed": curveBasisClosed, "basis-open": curveBasisOpen, bundle: curveBundle, cardinal: curveCardinal, "cardinal-closed": curveCardinalClosed, "cardinal-open": curveCardinalOpen, "catmull-rom": curveCatmullRom, "catmull-rom-closed": curveCatmullRomClosed, "catmull-rom-open": curveCatmullRomOpen, "monotone-x": curveMonotoneX, "monotone-y": curveMonotoneY, natural: curveNatural, "linear-closed": curveLinearClosed, linear: curveLinear, step: curveStep, "step-after": curveStepAfter, "step-before": curveStepBefore }; /** * Check if a target can use line-like grouped point offsets. * @param {object} $$ ChartInternal instance * @param {object|string} d Data value, target or id * @returns {boolean} Whether target uses point-like y coordinates * @private */ function isLinePointGroupType($$, d) { return $$.isLineType(d) || $$.isScatterType?.(d) || $$.isBubbleType?.(d); } /** * Get type filter for grouped line-like point offsets. * @param {object} $$ ChartInternal instance * @returns {function} Type filter * @private */ function getLinePointGroupTypeFilter($$) { return d => isLinePointGroupType($$, d); } /** * Get numeric value used for stacked offset calculation. * @param {object} $$ ChartInternal instance * @param {object} d Data row * @returns {number|Array|object|null} Offset value * @private */ function getShapeOffsetValue($$, d) { if ($$.isCandlestickType?.(d)) { return $$.getCandlestickData?.(d)?.close; } return $$.getBaseValue(d); } /** * Get grouped data point function for y coordinate * @param {object} d data vlaue * @returns {function|undefined} * @private */ function _getGroupedDataPointsFn(d) { const $$ = this; let fn; if (isLinePointGroupType($$, d)) { const typeFilter = getLinePointGroupTypeFilter($$); fn = $$.generateGetLinePoints($$.getShapeIndices(typeFilter), false, typeFilter); } else if ($$.isBarType(d)) { fn = $$.generateGetBarPoints($$.getShapeIndices($$.isBarType)); } else if ($$.isCandlestickType?.(d)) { fn = $$.generateGetCandlestickPoints?.($$.getShapeIndices($$.isCandlestickType)); } return fn; } /** * Get shape color with gradient support * @param {object} d Data object * @param {string} configKey Configuration key for linearGradient (e.g., 'bar_linearGradient', 'area_linearGradient') * @param {(d: IDataRow) => string | null} colorFn Fallback color function when gradient is not enabled * @returns {string | null} Color string or gradient URL * @private */ function getShapeColorWithGradient(d, configKey, colorFn) { return this.config[configKey] ? this.getGradienColortUrl(d.id) : colorFn(d); } /** * Initialize a shape element container * @param {ShapeElementConfig} config Configuration object * @private */ function initShapeElement(config) { const { $el } = this; const { elKey, className, cssRules, position } = config; const container = $el.main.select(`.${CLASS.chart}`); $el[elKey] = position === "first" ? container.insert("g", ":first-child") : container.append("g"); $el[elKey].attr("class", className); if (cssRules?.length) { $el[elKey].call(this.setCssRule(false, `.${className}`, cssRules)); } } /** * Common update targets pattern for shapes * @param {Array} targets Target data * @param {UpdateTargetsConfig} config Configuration object * @returns {d3Selection} Enter selection for additional setup * @private */ function updateTargetsForShape(targets, config) { const $$ = this; const { $el } = $$; const { type, elKey, containerClass, itemClass, initFn, withFocus = true, withStyles = true } = config; if (!$el[elKey]) { initFn.call($$); } const classChart = $$.getChartClass(type); const classFocus = withFocus ? $$.classFocus.bind($$) : () => ""; const mainUpdate = $el.main.select(`.${containerClass}`) .selectAll(`.${itemClass}`) .data($$.filterNullish(targets)) .attr("class", d => classChart(d) + classFocus(d)); const mainEnter = mainUpdate.enter().append("g") .attr("class", classChart); if (withStyles) { mainEnter .style("opacity", "0") .style("pointer-events", $$.getStylePropValue("none")); } return mainEnter; } var shape = { /** * Get the shape draw function * @returns {object} * @private */ getDrawShape() { const $$ = this; const isRotated = $$.config.axis_rotated; const { hasRadar, hasTreemap } = $$.state; const shape = { type: {}, indices: {}, pos: {} }; !hasTreemap && ["bar", "candlestick", "line", "area"].forEach(v => { const name = capitalize(v); if ($$.hasType(v) || $$.hasTypeOf(name) || (v === "line" && ($$.hasType("bubble") || $$.hasType("scatter")))) { const indices = $$.getShapeIndices($$[`is${name}Type`]); const drawFn = $$[`generateDraw${name}`]; shape.indices[v] = indices; shape.type[v] = drawFn ? drawFn.bind($$)(indices, false) : undefined; } }); if (!$$.hasArcType() || hasRadar || hasTreemap) { let cx; let cy; let xForText; let yForText; // generate circle x/y functions depending on updated params if (!hasTreemap) { cx = hasRadar ? $$.radarCircleX : (isRotated ? $$.circleY : $$.circleX); cy = hasRadar ? $$.radarCircleY : (isRotated ? $$.circleX : $$.circleY); } if (hasTreemap && $$.state.isCanvasMode) { xForText = yForText = function () { }; } else { xForText = $$.generateXYForText(shape.indices, true); yForText = $$.generateXYForText(shape.indices, false); } shape.pos = { xForText, yForText, cx: (cx || function () { }).bind($$), cy: (cy || function () { }).bind($$) }; } return shape; }, /** * Get shape's indices according it's position within each axis tick. * * From the below example, indices will be: * ==> {data1: 0, data2: 0, data3: 1, data4: 1, __max__: 1} * * data1 data3 data1 data3 * data2 data4 data2 data4 * ------------------------- * 0 1 * @param {function} typeFilter Chart type filter function * @returns {object} Indices object with its position */ getShapeIndices(typeFilter) { const $$ = this; const { config } = $$; const xs = config.data_xs; const hasXs = notEmpty(xs); const indices = {}; let i = hasXs ? {} : 0; if (hasXs) { getUnique(Object.keys(xs).map(v => xs[v])) .forEach(v => { i[v] = 0; indices[v] = {}; }); } $$.filterTargetsToShow($$.data.targets.filter(typeFilter, $$)) .forEach(d => { const xKey = d.id in xs ? xs[d.id] : ""; const ind = xKey ? indices[xKey] : indices; for (let j = 0, groups; (groups = config.data_groups[j]); j++) { if (groups.indexOf(d.id) < 0) { continue; } for (let k = 0, key; (key = groups[k]); k++) { if (key in ind) { ind[d.id] = ind[key]; break; } // for same grouped data, add other data to same indices if (d.id !== key && xKey) { ind[key] = ind[d.id] ?? i[xKey]; } } } if (isUndefined(ind[d.id])) { ind[d.id] = xKey ? i[xKey]++ : i++; ind.__max__ = (xKey ? i[xKey] : i) - 1; } }); return indices; }, /** * Get indices value based on data ID value * @param {object} indices Indices object * @param {object} d Data row * @param {string} caller Caller function name (Used only for 'sparkline' plugin) * @returns {object} Indices object * @private */ getIndices(indices, d, caller) { const $$ = this; const { data_xs: xs, bar_indices_removeNull: removeNull } = $$.config; const { id, index } = d; if ($$.isBarType(id) && removeNull) { const ind = {}; // redefine bar indices order $$.getAllValuesOnIndex(index, true) .forEach((v, i) => { ind[v.id] = i; ind.__max__ = i; }); return ind; } return notEmpty(xs) ? indices[xs[id]] : indices; }, /** * Get indices max number * @param {object} indices Indices object * @returns {number} Max number * @private */ getIndicesMax(indices) { if (!notEmpty(this.config.data_xs)) { return indices.__max__; } // if is multiple xs, return total sum of xs' __max__ value let total = 0; for (const key in indices) { total += indices[key].__max__ || 0; } return total; }, getShapeX(offset, indices, isSub) { const $$ = this; const { config, scale } = $$; const currScale = isSub ? scale.subX : (scale.zoom || scale.x); const barOverlap = config.bar_overlap; const barPadding = config.bar_padding; const sum = (p, c) => p + c; // total shapes half width const halfWidth = isObjectType(offset) && (offset._$total.length ? offset._$total.reduce(sum) / 2 : 0); // Pre-compute prefix sums to avoid O(n) slice+reduce on every bar datum const prefixSums = []; if (halfWidth && isObjectType(offset) && offset._$total.length) { let acc = 0; for (const v of offset._$total) { acc += v; prefixSums.push(acc); } } return d => { const ind = $$.getIndices(indices, d, "getShapeX"); const index = d.id in ind ? ind[d.id] : 0; const targetsNum = (ind.__max__ || 0) + 1; let x = 0; if (notEmpty(d.x)) { const xPos = currScale(d.x, true); if (halfWidth) { const offsetWidth = offset[d.id] || offset._$width; x = barOverlap ? xPos - offsetWidth / 2 : xPos - offsetWidth + (prefixSums[index] ?? offset._$total.slice(0, index + 1).reduce(sum)) - halfWidth; } else { x = xPos - (isNumber(offset) ? offset : offset._$width) * (targetsNum / 2 - (barOverlap ? 1 : index)); } } // adjust x position for bar.padding option if (offset && x && targetsNum > 1 && barPadding) { if (index) { x += barPadding * index; } if (targetsNum > 2) { x -= (targetsNum - 1) * barPadding / 2; } else if (targetsNum === 2) { x -= barPadding / 2; } } return x; }; }, getShapeY(isSub) { const $$ = this; const isStackNormalized = $$.isStackNormalized(); return d => { let { value } = d; if (isNumber(d)) { value = d; } else if ($$.isAreaRangeType(d)) { value = $$.getBaseValue(d, "mid"); } else if (isStackNormalized) { value = $$.getRatio("index", d, true); } else if ($$.isBubbleZType(d)) { value = $$.getBubbleZData(d.value, "y"); } else if ($$.isBarRangeType(d)) { // TODO use range.getEnd() like method value = value[1]; } return $$.getYScaleById(d.id, isSub)(value); }; }, /** * Get shape based y Axis min value * @param {string} id Data id * @returns {number} * @private */ getShapeYMin(id) { const $$ = this; const axisId = $$.axis.getId(id); const scale = $$.scale[axisId]; const [yMin] = scale.domain(); const inverted = $$.config[`axis_${axisId}_inverted`]; return !$$.isGrouped(id) && !inverted && yMin > 0 ? yMin : 0; }, /** * Get Shape's offset data * @param {function} typeFilter Type filter function * @returns {object} * @private */ getShapeOffsetData(typeFilter) { const $$ = this; const targets = $$.orderTargets($$.filterTargetsToShow($$.data.targets.filter(typeFilter, $$))); // Same IDs can receive new values through load()/flow(), so ID-only // caching can leave stacked offsets pointing at stale row maps. const dataGeneration = $$.state.dataGeneration; const targetIds = targets.map(t => t.id).join("_"); const cacheKey = `${KEY.shapeOffset}_${targetIds}`; // Check if result is already cached const cachedData = $$.cache.get(cacheKey); if (cachedData?.generation === dataGeneration) { return cachedData; } const isStackNormalized = $$.isStackNormalized(); const shapeOffsetTargets = targets.map(target => { let rowValues = target.values; const values = {}; if ($$.isStepType(target)) { rowValues = $$.convertValuesToStep(rowValues); } const rowValueMapByXValue = rowValues.reduce((out, d) => { const key = Number(d.x); const value = getShapeOffsetValue($$, d); out[key] = d; values[key] = isStackNormalized ? $$.getRatio("index", d, true) : value; return out; }, {}); return { id: target.id, rowValues, rowValueMapByXValue, values }; }); const indexMapByTargetId = targets.reduce((out, { id }, index) => { out[id] = index; return out; }, {}); const result = { generation: dataGeneration, indexMapByTargetId, shapeOffsetTargets }; // Cache the result $$.cache.add(cacheKey, result); return result; }, getShapeOffset(typeFilter, indices, isSub) { const $$ = this; const { shapeOffsetTargets, indexMapByTargetId } = $$.getShapeOffsetData(typeFilter); const groupsZeroAs = $$.config.data_groupsZeroAs; // Pre-build per-series same-stacking-group lookup to avoid .filter() on every datum. // bar_indices_removeNull recomputes group membership per-datum index, so fall back there. let sameGroupByTargetId = null; if (!$$.config.bar_indices_removeNull) { sameGroupByTargetId = new Map(); for (const target of shapeOffsetTargets) { const ind = $$.getIndices(indices, { id: target.id, index: 0 }); sameGroupByTargetId.set(target.id, shapeOffsetTargets.filter(t => t.id !== target.id && ind[t.id] === ind[target.id])); } } return (d, idx) => { const { id, value, x } = d; const baseValue = getShapeOffsetValue($$, d); const ind = $$.getIndices(indices, d); const scale = $$.getYScaleById(id, isSub); if ($$.isBarRangeType(d)) { // TODO use range.getStart() return scale(value[0]); } const dataXAsNumber = Number(x); const y0 = scale(groupsZeroAs === "zero" ? 0 : $$.getShapeYMin(id)); let offset = y0; const sameGroupTargets = sameGroupByTargetId?.get(id) ?? shapeOffsetTargets.filter(t => t.id !== id && ind[t.id] === ind[id]); for (const t of sameGroupTargets) { const { id: tid, rowValueMapByXValue, rowValues, values: tvalues } = t; // for same stacked group (ind[tid] === ind[id]) if (indexMapByTargetId[tid] < indexMapByTargetId[id]) { const rValue = tvalues[dataXAsNumber]; let row = rowValues[idx]; // check if the x values line up if (!row || Number(row.x) !== dataXAsNumber) { row = rowValueMapByXValue[dataXAsNumber]; } const rowValue = row && getShapeOffsetValue($$, row); if (isNumber(rowValue) && isNumber(baseValue) && rowValue * baseValue >= 0 && isNumber(rValue)) { const addOffset = baseValue === 0 ? ((groupsZeroAs === "positive" && rValue > 0) || (groupsZeroAs === "negative" && rValue < 0)) : true; if (addOffset) { offset += scale(rValue) - y0; } } } } return offset; }; }, /** * Generate line coordinate points from shared geometry. * @param {object} lineIndices Data order within x axis * @param {boolean} isSub Whether the coordinates are for subchart * @param {function} typeFilter Type filter for offset targets * @returns {function} Line point generator * @private */ generateGetLinePoints(lineIndices, isSub, typeFilter) { const $$ = this; const { config } = $$; const x = $$.getShapeX(0, lineIndices, isSub); const y = $$.getShapeY(isSub); const lineOffset = $$.getShapeOffset(typeFilter || $$.isLineType, lineIndices, isSub); const yScale = $$.getYScaleById.bind($$); return (d, i) => { const y0 = yScale.call($$, d.id, isSub)($$.getShapeYMin(d.id)); const offset = lineOffset(d, i) || y0; const posX = x(d); let posY = y(d); if (config.axis_rotated && ((d.value > 0 && posY < y0) || (d.value < 0 && y0 < posY))) { posY = y0; } const point = [posX, posY - (y0 - offset)]; return [ point, point, point, point ]; }; }, /** * Generate area coordinate points from shared geometry. * @param {object} areaIndices Data order within x axis * @param {boolean} isSub Whether the coordinates are for subchart * @returns {function} Area point generator * @private */ generateGetAreaPoints(areaIndices, isSub) { const $$ = this; const { config } = $$; const x = $$.getShapeX(0, areaIndices, isSub); const y = $$.getShapeY(!!isSub); const areaOffset = $$.getShapeOffset($$.isAreaType, areaIndices, isSub); const yScale = $$.getYScaleById.bind($$); return function (d, i) { const y0 = yScale.call($$, d.id, isSub)($$.getShapeYMin(d.id)); const offset = areaOffset(d, i) || y0; const posX = x(d); const value = d.value; let posY = y(d); if (config.axis_rotated && ((value > 0 && posY < y0) || (value < 0 && y0 < posY))) { posY = y0; } return [ [posX, offset], [posX, posY - (y0 - offset)], [posX, posY - (y0 - offset)], [posX, offset] ]; }; }, /** * Generate bar coordinate points from shared geometry. * @param {object} barIndices Data order within x axis * @param {boolean} isSub Whether the coordinates are for subchart * @returns {function} Bar point generator * @private */ generateGetBarPoints(barIndices, isSub) { const $$ = this; const { config } = $$; const axis = isSub ? $$.axis.subX : $$.axis.x; const barTargetsNum = $$.getIndicesMax(barIndices) + 1; const barW = $$.getBarW("bar", axis, barTargetsNum); const barX = $$.getShapeX(barW, barIndices, !!isSub); const barY = $$.getShapeY(!!isSub); const barOffset = $$.getShapeOffset($$.isBarType, barIndices, !!isSub); const yScale = $$.getYScaleById.bind($$); return (d, i) => { const { id } = d; const y0 = yScale.call($$, id, isSub)($$.getShapeYMin(id)); const offset = barOffset(d, i) || y0; const width = isNumber(barW) ? barW : barW[d.id] || barW._$width; const isInverted = config[`axis_${$$.axis.getId(id)}_inverted`]; const value = d.value; const posX = barX(d); let posY = barY(d); if (config.axis_rotated && !isInverted && ((value > 0 && posY < y0) || (value < 0 && y0 < posY))) { posY = y0; } if (!$$.isBarRangeType(d)) { posY -= y0 - offset; } const startPosX = posX + width; return [ [posX, offset], [posX, posY], [startPosX, posY], [startPosX, offset] ]; }; }, /** * Get data's y coordinate * @param {object} d Target data * @param {number} i Index number * @returns {number} y coordinate * @private */ circleY(d, i) { const $$ = this; const id = d.id; let points; if ($$.isGrouped(id)) { points = _getGroupedDataPointsFn.bind($$)(d); } return points ? points(d, i)[0][1] : $$.getYScaleById(id)($$.getBaseValue(d)); }, /** * Get data point x coordinate. * @param {object} d Data row * @returns {number|null} X coordinate * @private */ circleX(d) { return this.xx(d); }, /** * Generate data point y coordinate accessor. * @param {boolean} isSub Whether the coordinates are for subchart * @returns {function} Y coordinate accessor * @private */ updateCircleY(isSub = false) { const $$ = this; const typeFilter = getLinePointGroupTypeFilter($$); const getPoints = $$.generateGetLinePoints($$.getShapeIndices(typeFilter), isSub, typeFilter); return (d, i) => { const id = d.id; return $$.isGrouped(id) && isLinePointGroupType($$, d) ? getPoints(d, i)[0][1] : $$.getYScaleById(id, isSub)($$.getBaseValue(d)); }; }, /** * Get point radius. * @param {object} d Data row * @returns {number} Point radius * @private */ pointR(d) { const $$ = this; const { config } = $$; const pointR = config.point_r; let r = pointR; if ($$.isBubbleType(d)) { r = $$.getBubbleR(d); } else if (isFunction(pointR)) { r = pointR.bind($$.api)(d); } d.r = r; return r; }, /** * Get focused point radius. * @param {object} d Data row * @returns {number} Focused point radius * @private */ pointExpandedR(d) { const $$ = this; const { config } = $$; const scale = $$.isBubbleType(d) ? 1.15 : 1.75; return config.point_focus_expand_enabled ? (config.point_focus_expand_r || $$.pointR(d) * scale) : $$.pointR(d); }, /** * Get selected point radius. * @param {object} d Data row * @returns {number} Selected point radius * @private */ pointSelectR(d) { const $$ = this; const selectR = $$.config.point_select_r; return isFunction(selectR) ? selectR(d) : (selectR || $$.pointR(d) * 4); }, /** * Check if point.focus.only option can be applied. * @returns {boolean} Whether focus-only point rendering is active * @private */ isPointFocusOnly() { const $$ = this; return $$.config.point_focus_only && !$$.hasType("bubble") && !$$.hasType("scatter") && !$$.hasArcType(null, ["radar"]); }, /** * Get data point sensitivity radius. * @param {object} d Data point * @returns {number} Sensitivity radius * @private */ getPointSensitivity(d) { const $$ = this; let sensitivity = $$.config.point_sensitivity; if (!d) { return sensitivity; } else if (isFunction(sensitivity)) { sensitivity = sensitivity.call($$.api, d); } else if (sensitivity === "radius") { sensitivity = d.r; } return sensitivity; }, getBarW(type, axis, targetsNum) { const $$ = this; const { config, org, scale, state } = $$; const maxDataCount = $$.getMaxDataCount(); const isGrouped = type === "bar" && config.data_groups?.length; const configName = `${type}_width`; const { k } = $$.getZoomTransform?.() ?? { k: 1 }; const xMinMax = [ config.axis_x_min ?? org.xDomain[0], config.axis_x_max ?? org.xDomain[1] ].map(v => ($$.axis.isTimeSeries() ? parseDate.call($$, v) : Number(v))); let tickInterval = axis.tickInterval(maxDataCount); if (scale.zoom && !$$.axis.isCategorized() && k > 1) { const isSameMinMax = xMinMax.every((v, i) => v === org.xDomain[i]); tickInterval = org.xDomain.map((v, i) => { const value = isSameMinMax ? v : v - Math.abs(xMinMax[i]); return scale.zoom(value); }).reduce((a, c) => Math.abs(a) + c) / maxDataCount; } const getWidth = (id) => { const width = id ? config[configName][id] : config[configName]; const ratio = id ? width.ratio : config[`${configName}_ratio`]; const max = id ? width.max : config[`${configName}_max`]; const w = isNumber(width) ? width : (isFunction(width) ? width.call($$, state.width, targetsNum, maxDataCount) : (targetsNum ? (tickInterval * ratio) / targetsNum : 0)); return max && w > max ? max : w; }; let result = getWidth(); if (!isGrouped && isObjectType(config[configName])) { result = { _$width: result, _$total: [] }; $$.getTargetsToShow().forEach(v => { if (config[configName][v.id]) { result[v.id] = getWidth(v.id); result._$total.push(result[v.id] || result._$width); } }); } return result; }, /** * Get shape element * @param {string} shapeName Shape string * @param {number} i Index number * @param {string} id Data series id * @returns {d3Selection} * @private */ getShapeByIndex(shapeName, i, id) { const $$ = this; const { $el } = $$; const suffix = isValue(i) ? `-${i}` : ``; let shape = $el[shapeName]; // filter from shape reference if has if (shape && !shape.empty()) { shape = shape .filter(d => (id ? d.id === id : true)) .filter(d => (isValue(i) ? d.index === i : true)); } else { shape = (id ? $el.main .selectAll(`.${CLASS[`${shapeName}s`]}${$$.getTargetSelectorSuffix(id)}`) : $el.main) .selectAll(`.${CLASS[shapeName]}${suffix}`); } return shape; }, isWithinShape(that, d) { const $$ = this; const shape = select(that); let isWithin; if (!$$.isTargetToShow(d.id)) { isWithin = false; } else if ($$.hasValidPointType?.(that.nodeName)) { isWithin = $$.isStepType(d) ? $$.isWithinStep(that, $$.getYScaleById(d.id)($$.getBaseValue(d))) : $$.isWithinCircle(that, $$.isBubbleType(d) ? $$.pointSelectR(d) * 1.5 : 0); } else if (that.nodeName === "path") { isWithin = shape.classed(CLASS.bar) ? $$.isWithinBar(that) : true; } return isWithin; }, getInterpolate(d) { const $$ = this; const interpolation = $$.getInterpolateType(d); return CURVE_MAP[interpolation]; }, /** * Get curve generator for line-like shapes. * @param {object} d Data target * @returns {function} Curve generator * @private */ getCurve(d) { const $$ = this; const isRotatedStepType = $$.config.axis_rotated && $$.isStepType(d); // when is step & rotated, should be computed in different way // https://github.com/naver/billboard.js/issues/471 return isRotatedStepType ? context => { const step = $$.getInterpolate(d)(context); // keep the original method step.orgPoint = step.point; // to get rotated path data step.pointRotated = function (x, y) { this._point === 1 && (this._point = 2); const y1 = this._y * (1 - this._t) + y * this._t; this._context.lineTo(this._x, y1); this._context.lineTo(x, y1); this._x = x; this._y = y; }; step.point = function (x, y) { this._point === 0 ? this.orgPoint(x, y) : this.pointRotated(x, y); }; return step; } : $$.getInterpolate(d); }, getInterpolateType(d) { const $$ = this; const { config } = $$; const type = config.spline_interpolation_type; const interpolation = $$.isInterpolationType(type) ? type : "cardinal"; return $$.isSplineType(d) ? interpolation : ($$.isStepType(d) ? config.line_step_type : "linear"); }, isWithinBar(that) { const mouse = getPointer(this.state.event, that); const list = getRectSegList(that); const [seg0, seg1, seg2] = list; const x = Math.min(seg0.x, seg1.x); const y = Math.min(seg0.y, seg1.y); const offset = this.config.bar_sensitivity; const width = Math.abs(seg2.x - seg1.x); const height = Math.abs(seg0.y - seg1.y); const sx = x - offset; const ex = x + width + offset; const sy = y + height + offset; const ey = y - offset; const isWithin = sx < mouse[0] && mouse[0] < ex && ey < mouse[1] && mouse[1] < sy; return isWithin; } }; export { shape as default, getShapeColorWithGradient, initShapeElement, updateTargetsForShape };