Skip to main content
Module

x/billboardjs/ChartInternal/Axis/Axis.ts

πŸ“Š Re-usable, easy interface JavaScript chart library based on D3.js
Latest
File
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft, axisRight as d3AxisRight, axisTop as d3AxisTop} from "d3-axis";import type {AxisType} from "../../../types/types";import {$AXIS} from "../../config/classes";import { capitalize, isArray, isEmpty, isFunction, isNumber, isObjectType, isString, isValue, mergeObj, notEmpty, parseDate, sortValue} from "../../module/util";import {getScale} from "../internals/scale";import AxisRenderer from "./AxisRenderer";
export default { getAxisInstance: function() { return this.axis || new Axis(this); }};
class Axis { public owner;
public x; public subX; public y; public y2;
private axesList = {}; public tick = { x: null, y: null, y2: null }; public xs = []; private orient = { x: "bottom", y: "left", y2: "right", subX: "bottom" };
constructor(owner) { this.owner = owner; this.setOrient(); }
private getAxisClassName(id) { return `${$AXIS.axis} ${$AXIS[`axis${capitalize(id)}`]}`; }
private isHorizontal($$, forHorizontal) { const isRotated = $$.config.axis_rotated;
return forHorizontal ? isRotated : !isRotated; }
public isCategorized() { const {config, state} = this.owner;
return config.axis_x_type.indexOf("category") >= 0 || state.hasRadar; }
public isCustomX() { const {config} = this.owner;
return !this.isTimeSeries() && (config.data_x || notEmpty(config.data_xs)); }
public isTimeSeries(id = "x") { return this.owner.config[`axis_${id}_type`] === "timeseries"; }
public isLog(id = "x") { return this.owner.config[`axis_${id}_type`] === "log"; }
public isTimeSeriesY() { return this.isTimeSeries("y"); }
public getAxisType(id = "x"): string { let type = "linear";
if (this.isTimeSeries(id)) { type = this.owner.config.axis_x_localtime ? "time" : "utc"; } else if (this.isLog(id)) { type = "log"; }
return type; }
init() { const $$ = this.owner; const {config, $el: {main, axis}, state: {clip}} = $$; const isRotated = config.axis_rotated; const target = ["x", "y"];
config.axis_y2_show && target.push("y2");
target.forEach(v => { const classAxis = this.getAxisClassName(v); const classLabel = $AXIS[`axis${v.toUpperCase()}Label`];
axis[v] = main.append("g") .attr("class", classAxis) .attr("clip-path", () => { let res = null;
if (v === "x") { res = clip.pathXAxis; } else if (v === "y") { // || v === "y2") { res = clip.pathYAxis; }
return res; }) .attr("transform", $$.getTranslate(v)) .style("visibility", config[`axis_${v}_show`] ? null : "hidden");
axis[v].append("text") .attr("class", classLabel) .attr("transform", ["rotate(-90)", null][ v === "x" ? +!isRotated : +isRotated ]) .style("text-anchor", () => this.textAnchorForAxisLabel(v));
this.generateAxes(v); });
config.axis_tooltip && this.setAxisTooltip(); }
/** * Set axis orient according option value * @private */ setOrient() { const $$ = this.owner; const { axis_rotated: isRotated, axis_y_inner: yInner, axis_y2_inner: y2Inner } = $$.config;
this.orient = { x: isRotated ? "left" : "bottom", y: isRotated ? (yInner ? "top" : "bottom") : (yInner ? "right" : "left"), y2: isRotated ? (y2Inner ? "bottom" : "top") : (y2Inner ? "left" : "right"), subX: isRotated ? "left" : "bottom" }; }
/** * Generate axes * It's used when axis' axes option is set * @param {string} id Axis id * @private */ generateAxes(id: string) { const $$ = this.owner; const {config} = $$; const axes: any[] = []; const axesConfig = config[`axis_${id}_axes`]; const isRotated = config.axis_rotated; let d3Axis;
if (id === "x") { d3Axis = isRotated ? d3AxisLeft : d3AxisBottom; } else if (id === "y") { d3Axis = isRotated ? d3AxisBottom : d3AxisLeft; } else if (id === "y2") { d3Axis = isRotated ? d3AxisTop : d3AxisRight; }
if (axesConfig.length) { axesConfig.forEach(v => { const tick = v.tick || {}; const scale = $$.scale[id].copy();
v.domain && scale.domain(v.domain);
axes.push( d3Axis(scale) .ticks(tick.count) .tickFormat( isFunction(tick.format) ? tick.format.bind($$.api) : ((x: any) => x) ) .tickValues(tick.values) .tickSizeOuter(tick.outer === false ? 0 : 6) ); }); }
this.axesList[id] = axes; }
/** * Update axes nodes * @private */ updateAxes() { const $$ = this.owner; const {config, $el: {main}, $T} = $$;
Object.keys(this.axesList).forEach(id => { const axesConfig = config[`axis_${id}_axes`]; const scale = $$.scale[id].copy(); const range = scale.range();
this.axesList[id].forEach((v, i) => { const axisRange = v.scale().range();
// adjust range value with the current // https://github.com/naver/billboard.js/issues/859 if (!range.every((v, i) => v === axisRange[i])) { v.scale().range(range); }
const className = `${this.getAxisClassName(id)}-${i + 1}`; let g = main.select(`.${className.replace(/\s/, ".")}`);
if (g.empty()) { g = main.append("g") .attr("class", className) .style("visibility", config[`axis_${id}_show`] ? null : "hidden") .call(v); } else { axesConfig[i].domain && scale.domain(axesConfig[i].domain);
$T(g).call(v.scale(scale)); }
g.attr("transform", $$.getTranslate(id, i + 1)); }); }); }
/** * Set Axis & tick values * called from: updateScales() * @param {string} id Axis id string * @param {d3Scale} scale Scale * @param {boolean} outerTick If show outer tick * @param {boolean} noTransition If with no transition * @private */ setAxis(id, scale, outerTick, noTransition): void { const $$ = this.owner;
if (id !== "subX") { this.tick[id] = this.getTickValues(id); }
// @ts-ignore this[id] = this.getAxis( id, scale, outerTick, // do not transit x Axis on zoom and resizing // https://github.com/naver/billboard.js/issues/1949 id === "x" && ($$.scale.zoom || $$.config.subchart_show || $$.state.resizing) ? true : noTransition ); }
// called from : getMaxTickSize() getAxis(id, scale, outerTick, noTransition, noTickTextRotate): AxisRenderer { const $$ = this.owner; const {config} = $$; const isX = /^(x|subX)$/.test(id); const type = isX ? "x" : id; const isCategory = isX && this.isCategorized(); const orient = this.orient[id]; const tickTextRotate = noTickTextRotate ? 0 : $$.getAxisTickRotate(type); let tickFormat;
if (isX) { tickFormat = (id === "subX") ? $$.format.subXAxisTick : $$.format.xAxisTick; } else { const fn = config[`axis_${id}_tick_format`];
if (isFunction(fn)) { tickFormat = fn.bind($$.api); } }
let tickValues = this.tick[type];
const axisParams = mergeObj({ outerTick, noTransition, config, id, tickTextRotate, owner: $$ }, isX && { isCategory, isInverted: config.axis_x_inverted, tickMultiline: config.axis_x_tick_multiline, tickWidth: config.axis_x_tick_width, tickTitle: isCategory && config.axis_x_tick_tooltip && $$.api.categories(), orgXScale: $$.scale.x });
if (!isX) { axisParams.tickStepSize = config[`axis_${type}_tick_stepSize`]; }
const axis = new AxisRenderer(axisParams) .scale((isX && $$.scale.zoom) || scale) .orient(orient);
if (isX && this.isTimeSeries() && tickValues && !isFunction(tickValues)) { const fn = parseDate.bind($$);
tickValues = tickValues.map(v => fn(v)); } else if (!isX && this.isTimeSeriesY()) { // https://github.com/d3/d3/blob/master/CHANGES.md#time-intervals-d3-time axis.ticks(config.axis_y_tick_time_value); tickValues = null; }
tickValues && axis.tickValues(tickValues);
// Set tick axis.tickFormat( tickFormat || ( !isX && ($$.isStackNormalized() && (x => `${x}%`)) ) );
if (isCategory) { axis.tickCentered(config.axis_x_tick_centered);
if (isEmpty(config.axis_x_tick_culling)) { config.axis_x_tick_culling = false; } }
const tickCount = config[`axis_${type}_tick_count`];
tickCount && axis.ticks(tickCount);
return axis; }
updateXAxisTickValues(targets, axis?): string[] { const $$ = this.owner; const {config} = $$; const fit = config.axis_x_tick_fit; let count = config.axis_x_tick_count; let values;
if (fit || (count && fit)) { values = $$.mapTargetsToUniqueXs(targets);
// if given count is greater than the value length, then limit the count. if (this.isCategorized() && count > values.length) { count = values.length; }
values = this.generateTickValues( values, count, this.isTimeSeries() ); }
if (axis) { axis.tickValues(values); } else if (this.x) { this.x.tickValues(values); this.subX?.tickValues(values); }
return values; }
getId(id: string): string { const {config, scale} = this.owner; let axis = config.data_axes[id];
// when data.axes option has 'y2', but 'axis.y2.show=true' isn't set will return 'y' if (!axis || !scale[axis]) { axis = "y"; }
return axis; }
getXAxisTickFormat(forSubchart?: boolean): Function { const $$ = this.owner; const {config, format} = $$; // enable different tick format for x and subX - subX format defaults to x format if not defined const tickFormat = forSubchart ? config.subchart_axis_x_tick_format || config.axis_x_tick_format : config.axis_x_tick_format; const isTimeSeries = this.isTimeSeries(); const isCategorized = this.isCategorized(); let currFormat;
if (tickFormat) { if (isFunction(tickFormat)) { currFormat = tickFormat.bind($$.api); } else if (isTimeSeries) { currFormat = date => (date ? format.axisTime(tickFormat)(date) : ""); } } else { currFormat = isTimeSeries ? format.defaultAxisTime : ( isCategorized ? $$.categoryName : v => (v < 0 ? v.toFixed(0) : v) ); }
return isFunction(currFormat) ? v => currFormat.apply($$, isCategorized ? [v, $$.categoryName(v)] : [v]) : currFormat; }
getTickValues(id: string) { const $$ = this.owner; const tickValues = $$.config[`axis_${id}_tick_values`]; const axis = $$[`${id}Axis`];
return (isFunction(tickValues) ? tickValues.call($$.api) : tickValues) || (axis ? axis.tickValues() : undefined); }
getLabelOptionByAxisId(id: string) { return this.owner.config[`axis_${id}_label`]; }
getLabelText(id: string) { const option = this.getLabelOptionByAxisId(id);
return isString(option) ? option : ( option ? option.text : null ); }
setLabelText(id: string, text: string) { const $$ = this.owner; const {config} = $$; const option = this.getLabelOptionByAxisId(id);
if (isString(option)) { config[`axis_${id}_label`] = text; } else if (option) { option.text = text; } }
getLabelPosition(id: string, defaultPosition) { const isRotated = this.owner.config.axis_rotated; const option = this.getLabelOptionByAxisId(id); const position = (isObjectType(option) && option.position) ? option.position : defaultPosition[+!isRotated];
const has = v => !!~position.indexOf(v);
return { isInner: has("inner"), isOuter: has("outer"), isLeft: has("left"), isCenter: has("center"), isRight: has("right"), isTop: has("top"), isMiddle: has("middle"), isBottom: has("bottom") }; }
getAxisLabelPosition(id: string) { return this.getLabelPosition(id, id === "x" ? ["inner-top", "inner-right"] : ["inner-right", "inner-top"]); }
getLabelPositionById(id: string) { return this.getAxisLabelPosition(id); }
xForAxisLabel(id: string) { const $$ = this.owner; const {state: {width, height}} = $$; const position = this.getAxisLabelPosition(id); let x = position.isMiddle ? -height / 2 : 0;
if (this.isHorizontal($$, id !== "x")) { x = position.isLeft ? 0 : ( position.isCenter ? width / 2 : width ); } else if (position.isBottom) { x = -height; }
return x; }
dxForAxisLabel(id: string) { const $$ = this.owner; const position = this.getAxisLabelPosition(id); let dx = position.isBottom ? "0.5em" : "0";
if (this.isHorizontal($$, id !== "x")) { dx = position.isLeft ? "0.5em" : ( position.isRight ? "-0.5em" : "0" ); } else if (position.isTop) { dx = "-0.5em"; }
return dx; }
textAnchorForAxisLabel(id: string) { const $$ = this.owner; const position = this.getAxisLabelPosition(id); let anchor = position.isMiddle ? "middle" : "end";
if (this.isHorizontal($$, id !== "x")) { anchor = position.isLeft ? "start" : ( position.isCenter ? "middle" : "end" ); } else if (position.isBottom) { anchor = "start"; }
return anchor; }
dyForAxisLabel(id: AxisType) { const $$ = this.owner; const {config} = $$; const isRotated = config.axis_rotated; const isInner = this.getAxisLabelPosition(id).isInner; const tickRotate = config[`axis_${id}_tick_rotate`] ? $$.getHorizontalAxisHeight(id) : 0; const {width: maxTickWidth} = this.getMaxTickSize(id); let dy;
if (id === "x") { const xHeight = config.axis_x_height;
if (isRotated) { dy = isInner ? "1.2em" : -25 - maxTickWidth; } else if (isInner) { dy = "-0.5em"; } else if (xHeight) { dy = xHeight - 10; } else if (tickRotate) { dy = tickRotate - 10; } else { dy = "3em"; } } else { dy = { y: ["-0.5em", 10, "3em", "1.2em", 10], y2: ["1.2em", -20, "-2.2em", "-0.5em", 15] }[id];
if (isRotated) { if (isInner) { dy = dy[0]; } else if (tickRotate) { dy = tickRotate * (id === "y2" ? -1 : 1) - dy[1]; } else { dy = dy[2]; } } else { dy = isInner ? dy[3] : ( dy[4] + ( config[`axis_${id}_inner`] ? 0 : (maxTickWidth + dy[4]) ) ) * (id === "y" ? -1 : 1); } }
return dy; }
/** * Get max tick size * @param {string} id axis id string * @param {boolean} withoutRecompute wheather or not to recompute * @returns {object} {width, height} * @private */ getMaxTickSize(id: AxisType, withoutRecompute?: boolean): {width: number, height: number} { const $$ = this.owner; const {config, state: {current}, $el: {svg, chart}} = $$; const currentTickMax = current.maxTickSize[id]; const configPrefix = `axis_${id}`; const max = { width: 0, height: 0 };
if ( withoutRecompute || !config[`${configPrefix}_show`] || ( currentTickMax.width > 0 && $$.filterTargetsToShow().length === 0 ) ) { return currentTickMax; }
if (svg) { const isYAxis = /^y2?$/.test(id); const targetsToShow = $$.filterTargetsToShow($$.data.targets); const scale = $$.scale[id].copy().domain( $$[`get${isYAxis ? "Y" : "X"}Domain`](targetsToShow, id) ); const domain = scale.domain();
const isDomainSame = domain[0] === domain[1] && domain.every(v => v > 0); const isCurrentMaxTickDomainSame = isArray(currentTickMax.domain) && currentTickMax.domain[0] === currentTickMax.domain[1] && currentTickMax.domain.every(v => v > 0);
// do not compute if domain or currentMaxTickDomain is same if (isDomainSame || isCurrentMaxTickDomainSame) { return currentTickMax.size; } else { currentTickMax.domain = domain; }
// reset old max state value to prevent from new data loading if (!isYAxis) { currentTickMax.ticks.splice(0); }
const axis = this.getAxis(id, scale, false, false, true); const tickRotate = config[`${configPrefix}_tick_rotate`]; const tickCount = config[`${configPrefix}_tick_count`]; const tickValues = config[`${configPrefix}_tick_values`];
// Make to generate the final tick text to be rendered // https://github.com/naver/billboard.js/issues/920 // Do not generate if 'tick values' option is given // https://github.com/naver/billboard.js/issues/1251 if (!tickValues && tickCount) { axis.tickValues( this.generateTickValues( domain, tickCount, isYAxis ? this.isTimeSeriesY() : this.isTimeSeries() ) ); }
!isYAxis && this.updateXAxisTickValues(targetsToShow, axis);
const dummy = chart.append("svg") .style("visibility", "hidden") .style("position", "fixed") .style("top", "0") .style("left", "0");
axis.create(dummy);
dummy.selectAll("text") .attr("transform", isNumber(tickRotate) ? `rotate(${tickRotate})` : null) .each(function(d, i) { const {width, height} = this.getBoundingClientRect();
max.width = Math.max(max.width, width); max.height = Math.max(max.height, height);
// cache tick text width for getXAxisTickTextY2Overflow() if (!isYAxis) { currentTickMax.ticks[i] = width; } });
dummy.remove(); }
Object.keys(max).forEach(key => { if (max[key] > 0) { currentTickMax[key] = max[key]; } });
return currentTickMax; }
getXAxisTickTextY2Overflow(defaultPadding) { const $$ = this.owner; const {axis, config, state: {current, isLegendRight, legendItemWidth}} = $$; const xAxisTickRotate = $$.getAxisTickRotate("x"); const positiveRotation = xAxisTickRotate > 0 && xAxisTickRotate < 90;
if ( (axis.isCategorized() || axis.isTimeSeries()) && config.axis_x_tick_fit && (!config.axis_x_tick_culling || isEmpty(config.axis_x_tick_culling)) && !config.axis_x_tick_multiline && positiveRotation ) { const y2AxisWidth = (config.axis_y2_show && current.maxTickSize.y2.width) || 0; const legendWidth = (isLegendRight && legendItemWidth) || 0; const widthWithoutCurrentPaddingLeft = current.width - $$.getCurrentPaddingByDirection("left"); const maxOverflow = this.getXAxisTickMaxOverflow( xAxisTickRotate, widthWithoutCurrentPaddingLeft - defaultPadding ) - y2AxisWidth - legendWidth; const xAxisTickTextY2Overflow = Math.max(0, maxOverflow) + defaultPadding; // for display inconsistencies between browsers
return Math.min(xAxisTickTextY2Overflow, widthWithoutCurrentPaddingLeft / 2); }
return 0; }
getXAxisTickMaxOverflow(xAxisTickRotate, widthWithoutCurrentPaddingLeft) { const $$ = this.owner; const {axis, config, state} = $$; const isTimeSeries = axis.isTimeSeries();
const tickTextWidths = state.current.maxTickSize.x.ticks; const tickCount = tickTextWidths.length; const {left, right} = state.axis.x.padding; let maxOverflow = 0;
const remaining = tickCount - (isTimeSeries && config.axis_x_tick_fit ? 0.5 : 0);
for (let i = 0; i < tickCount; i++) { const tickIndex = i + 1; const rotatedTickTextWidth = Math.cos(Math.PI * xAxisTickRotate / 180) * tickTextWidths[i]; const ticksBeforeTickText = tickIndex - (isTimeSeries ? 1 : 0.5) + left;
// Skip ticks if there are no ticks before them if (ticksBeforeTickText <= 0) { continue; }
const xAxisLengthWithoutTickTextWidth = widthWithoutCurrentPaddingLeft - rotatedTickTextWidth; const tickLength = xAxisLengthWithoutTickTextWidth / ticksBeforeTickText; const remainingTicks = remaining - tickIndex;
const paddingRightLength = right * tickLength; const remainingTickWidth = (remainingTicks * tickLength) + paddingRightLength; const overflow = rotatedTickTextWidth - (tickLength / 2) - remainingTickWidth;
maxOverflow = Math.max(maxOverflow, overflow); }
const filteredTargets = $$.filterTargetsToShow($$.data.targets); let tickOffset = 0;
if ( !isTimeSeries && config.axis_x_tick_count <= filteredTargets.length && filteredTargets[0].values.length ) { const scale = getScale($$.axis.getAxisType("x"), 0, widthWithoutCurrentPaddingLeft - maxOverflow) .domain([ left * -1, $$.getXDomainMax($$.data.targets) + 1 + right ]);
tickOffset = Math.ceil((scale(1) - scale(0)) / 2); }
return maxOverflow + tickOffset; }
updateLabels(withTransition) { const $$ = this.owner; const {$el: {main}, $T} = $$;
const labels = { x: main.select(`.${$AXIS.axisX} .${$AXIS.axisXLabel}`), y: main.select(`.${$AXIS.axisY} .${$AXIS.axisYLabel}`), y2: main.select(`.${$AXIS.axisY2} .${$AXIS.axisY2Label}`) };
Object.keys(labels).filter(id => !labels[id].empty()) .forEach((v: AxisType) => { const node = labels[v];
// @check $$.$T(node, withTransition) $T(node, withTransition) .attr("x", () => this.xForAxisLabel(v)) .attr("dx", () => this.dxForAxisLabel(v)) .attr("dy", () => this.dyForAxisLabel(v)) .text(() => this.getLabelText(v)); }); }
/** * Get axis padding value * @param {number|object} padding Padding object * @param {string} key Key string of padding * @param {Date|number} defaultValue Default value * @param {number} domainLength Domain length * @returns {number} Padding value in scale * @private */ getPadding(padding: number | {[key: string]: number}, key: string, defaultValue: number, domainLength: number): number { const p = isNumber(padding) ? padding : padding[key];
if (!isValue(p)) { return defaultValue; }
return this.owner.convertPixelToScale( /(bottom|top)/.test(key) ? "y" : "x", p, domainLength ); }
generateTickValues(values, tickCount, forTimeSeries) { let tickValues = values;
if (tickCount) { const targetCount = isFunction(tickCount) ? tickCount() : tickCount;
// compute ticks according to tickCount if (targetCount === 1) { tickValues = [values[0]]; } else if (targetCount === 2) { tickValues = [values[0], values[values.length - 1]]; } else if (targetCount > 2) { const isCategorized = this.isCategorized();
const count = targetCount - 2; const start = values[0]; const end = values[values.length - 1]; const interval = (end - start) / (count + 1); let tickValue;
// re-construct unique values tickValues = [start];
for (let i = 0; i < count; i++) { tickValue = +start + interval * (i + 1); tickValues.push( forTimeSeries ? new Date(tickValue) : ( isCategorized ? Math.round(tickValue) : tickValue ) ); }
tickValues.push(end); } }
if (!forTimeSeries) { tickValues = tickValues.sort((a, b) => a - b); }
return tickValues; }
generateTransitions(withTransition) { const $$ = this.owner; const {$el: {axis}, $T} = $$;
const [axisX, axisY, axisY2, axisSubX] = ["x", "y", "y2", "subX"] .map(v => $T(axis[v], withTransition));
return {axisX, axisY, axisY2, axisSubX}; }
redraw(transitions, isHidden, isInit) { const $$ = this.owner; const {config, $el} = $$; const opacity = isHidden ? "0" : null;
["x", "y", "y2", "subX"].forEach(id => { const axis = this[id]; const $axis = $el.axis[id];
if (axis && $axis) { if (!isInit && !config.transition_duration) { axis.config.withoutTransition = true; }
$axis.style("opacity", opacity); axis.create(transitions[`axis${capitalize(id)}`]); } });
this.updateAxes(); }
/** * Redraw axis * @param {Array} targetsToShow targets data to be shown * @param {object} wth option object * @param {d3.Transition} transitions Transition object * @param {object} flow flow object * @param {boolean} isInit called from initialization * @private */ redrawAxis(targetsToShow, wth, transitions, flow, isInit: boolean): void { const $$ = this.owner; const {config, scale, $el} = $$; const hasZoom = !!scale.zoom; let xDomainForZoom;
if (!hasZoom && this.isCategorized() && targetsToShow.length === 0) { scale.x.domain([0, $el.axis.x.selectAll(".tick").size()]); }
if (scale.x && targetsToShow.length) { !hasZoom && $$.updateXDomain(targetsToShow, wth.UpdateXDomain, wth.UpdateOrgXDomain, wth.TrimXDomain);
if (!config.axis_x_tick_values) { this.updateXAxisTickValues(targetsToShow); } } else if (this.x) { this.x.tickValues([]); this.subX?.tickValues([]); }
if (config.zoom_rescale && !flow) { xDomainForZoom = scale.x.orgDomain(); }
["y", "y2"].forEach(key => { const prefix = `axis_${key}_`; const axisScale = scale[key];
if (axisScale) { const tickValues = config[`${prefix}tick_values`]; const tickCount = config[`${prefix}tick_count`];
axisScale.domain($$.getYDomain(targetsToShow, key, xDomainForZoom));
if (!tickValues && tickCount) { const axis = $$.axis[key]; const domain = axisScale.domain();
axis.tickValues( this.generateTickValues( domain, domain.every(v => v === 0) ? 1 : tickCount, this.isTimeSeriesY() ) ); } } });
// axes this.redraw(transitions, $$.hasArcType(), isInit);
// Update axis label this.updateLabels(wth.Transition);
// show/hide if manual culling needed if ((wth.UpdateXDomain || wth.UpdateXAxis || wth.Y) && targetsToShow.length) { this.setCulling(); }
// Update sub domain if (wth.Y) { scale.subY?.domain($$.getYDomain(targetsToShow, "y")); scale.subY2?.domain($$.getYDomain(targetsToShow, "y2")); } }
/** * Set manual culling * @private */ setCulling() { const $$ = this.owner; const {config, state: {clip, current}, $el} = $$;
["subX", "x", "y", "y2"].forEach(type => { const axis = $el.axis[type];
// subchart x axis should be aligned with x axis culling const id = type === "subX" ? "x" : type;
const cullingOptionPrefix = `axis_${id}_tick_culling`; const toCull = config[cullingOptionPrefix];
if (axis && toCull) { const tickNodes = axis.selectAll(".tick"); const tickValues = sortValue(tickNodes.data()); const tickSize = tickValues.length; const cullingMax = config[`${cullingOptionPrefix}_max`]; const lines = config[`${cullingOptionPrefix}_lines`]; let intervalForCulling;
if (tickSize) { for (let i = 1; i < tickSize; i++) { if (tickSize / i < cullingMax) { intervalForCulling = i; break; } }
tickNodes .each(function(d) { const node = lines ? this.querySelector("text") : this;
if (node) { node.style.display = tickValues.indexOf(d) % intervalForCulling ? "none" : null; } }); } else { tickNodes.style("display", null); }
// set/unset x_axis_tick_clippath if (type === "x") { const clipPath = current.maxTickSize.x.clipPath ? clip.pathXAxisTickTexts : null;
$el.svg.selectAll(`.${$AXIS.axisX} .tick text`) .attr("clip-path", clipPath); } } }); }
/** * Set axis tooltip * @private */ setAxisTooltip(): void { const $$ = this.owner; const {config: {axis_rotated: isRotated, axis_tooltip}, $el: {axis, axisTooltip}} = $$; const bgColor = axis_tooltip.backgroundColor ?? "black";
$$.generateTextBGColorFilter( bgColor, { x: -0.15, y: -0.2, width: 1.3, height: 1.3 } );
["x", "y", "y2"].forEach(v => { axisTooltip[v] = axis[v]?.append("text") .classed($AXIS[`axis${v.toUpperCase()}Tooltip`], true) .attr("filter", $$.updateTextBGColor({id: v}, bgColor));
if (isRotated) { const pos = v === "x" ? "x" : "y"; const val = v === "y" ? "1.15em" : (v === "x" ? "-0.3em" : "-0.4em");
axisTooltip[v]?.attr(pos, val) .attr(`d${v === "x" ? "y" : "x"}`, v === "x" ? "0.4em" : "-1.3em") .style("text-anchor", v === "x" ? "end" : null); } else { const pos = v === "x" ? "y" : "x"; const val = v === "x" ? "1.15em" : `${v === "y" ? "-" : ""}0.4em`;
axisTooltip[v]?.attr(pos, val) .attr(`d${v === "x" ? "x" : "y"}`, v === "x" ? "-1em" : "0.3em") .style("text-anchor", v === "y" ? "end" : null); } }); }}