Skip to content

Commit

Permalink
feat(api): Intent to ship subchart method
Browse files Browse the repository at this point in the history
Implement .subchart() and .subchart.reset()

Close #3342
  • Loading branch information
netil authored and netil committed Sep 5, 2023
1 parent e4b77aa commit ac9e229
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 142 deletions.
242 changes: 152 additions & 90 deletions src/Chart/api/subchart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,161 @@
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {extend, parseDate} from "../../module/util";
import {$COMMON} from "../../config/classes";
import type {TDomain} from "../../ChartInternal/data/IData";

export default {
subchart: {
/**
* Show subchart
* - **NOTE:** for ESM imports, needs to import 'subchart' exports and instantiate it by calling `subchart()`.
* @function subchart鈥how
* @instance
* @memberof Chart
* @example
* // for ESM imports, needs to import 'subchart' and must be instantiated first to enable subchart's API.
* import {subchart} from "billboard.js";
*
* const chart = bb.generate({
* ...
* subchart: {
* // need to be instantiated by calling 'subchart()'
* enabled: subchart()
*
* // in case don't want subchart to be shown at initialization, instantiate with '!subchart()'
* enabled: !subchart()
* }
* });
*
* chart.subchart.show();
*/
show(): void {
const $$ = this.internal;
const {$el: {subchart}, config} = $$;
const show = config.subchart_show;

if (!show) {
// unbind zoom event bound to chart rect area
$$.unbindZoomEvent();

config.subchart_show = !show;
!subchart.main && $$.initSubchart();

let $target = subchart.main.selectAll(`.${$COMMON.target}`);

// need to cover when new data has been loaded
if ($$.data.targets.length !== $target.size()) {
$$.updateSizes();
$$.updateTargetsForSubchart($$.data.targets);

$target = subchart.main?.selectAll(`.${$COMMON.target}`);
}

$target?.style("opacity", null);
subchart.main?.style("display", null);

this.resize();
}
},

/**
* Hide generated subchart
* - **NOTE:** for ESM imports, needs to import 'subchart' exports and instantiate it by calling `subchart()`.
* @function subchart鈥ide
* @instance
* @memberof Chart
* @example
* chart.subchart.hide();
*/
hide(): void {
const $$ = this.internal;
const {$el: {subchart: {main}}, config} = $$;

if (config.subchart_show && main?.style("display") !== "none") {
config.subchart_show = false;
main.style("display", "none");

this.resize();
/**
* Select subchart by giving x domain range.
* @function subchart
* @instance
* @memberof Chart
* @param {Array} domainValue If domain range is given, the subchart will be seleted to the given domain. If no argument is given, the current subchart selection domain will be returned.
* @returns {Array} domain value in array
* @example
* // Specify domain for subchart selection
* chart.subchart([1, 2]);
*
* // Get the current subchart selection domain range
* chart.subchart();
*/
// NOTE: declared funciton assigning to variable to prevent duplicated method generation in JSDoc.
const subchart = function<T = TDomain[]>(domainValue?: T): T | undefined {
const $$ = this.internal;
const {axis, brush, config, scale: {x, subX}} = $$;
let domain: any = domainValue;

if (config.subchart_show && Array.isArray(domain)) {
if (axis.isTimeSeries()) {
domain = domain.map(x => parseDate.bind($$)(x));
}

const isWithinRange = $$.withinRange(
domain,
$$.getZoomDomain("subX", true),
$$.getZoomDomain("subX")
);

isWithinRange && brush.move(
brush.getSelection(),
domain.map(subX)
);
} else {
domain = x.orgDomain();
}

return domain as T;
};

extend(subchart, {
/**
* Show subchart
* - **NOTE:** for ESM imports, needs to import 'subchart' exports and instantiate it by calling `subchart()`.
* @function subchart鈥how
* @instance
* @memberof Chart
* @example
* // for ESM imports, needs to import 'subchart' and must be instantiated first to enable subchart's API.
* import {subchart} from "billboard.js";
*
* const chart = bb.generate({
* ...
* subchart: {
* // need to be instantiated by calling 'subchart()'
* enabled: subchart()
*
* // in case don't want subchart to be shown at initialization, instantiate with '!subchart()'
* enabled: !subchart()
* }
* });
*
* chart.subchart.show();
*/
show(): void {
const $$ = this.internal;
const {$el: {subchart}, config} = $$;
const show = config.subchart_show;

if (!show) {
// unbind zoom event bound to chart rect area
$$.unbindZoomEvent();

config.subchart_show = !show;
!subchart.main && $$.initSubchart();

let $target = subchart.main.selectAll(`.${$COMMON.target}`);

// need to cover when new data has been loaded
if ($$.data.targets.length !== $target.size()) {
$$.updateSizes();
$$.updateTargetsForSubchart($$.data.targets);

$target = subchart.main?.selectAll(`.${$COMMON.target}`);
}
},

/**
* Toggle the visiblity of subchart
* - **NOTE:** for ESM imports, needs to import 'subchart' exports and instantiate it by calling `subchart()`.
* @function subchart鈥oggle
* @instance
* @memberof Chart
* @example
* // When subchart is hidden, will be shown
* // When subchart is shown, will be hidden
* chart.subchart.toggle();
*/
toggle(): void {
const $$ = this.internal;
const {config} = $$;

this.subchart[config.subchart_show ? "hide" : "show"]();

$target?.style("opacity", null);
subchart.main?.style("display", null);

this.resize();
}
},

/**
* Hide generated subchart
* - **NOTE:** for ESM imports, needs to import 'subchart' exports and instantiate it by calling `subchart()`.
* @function subchart鈥ide
* @instance
* @memberof Chart
* @example
* chart.subchart.hide();
*/
hide(): void {
const $$ = this.internal;
const {$el: {subchart: {main}}, config} = $$;

if (config.subchart_show && main?.style("display") !== "none") {
config.subchart_show = false;
main.style("display", "none");

this.resize();
}
},

/**
* Toggle the visiblity of subchart
* - **NOTE:** for ESM imports, needs to import 'subchart' exports and instantiate it by calling `subchart()`.
* @function subchart鈥oggle
* @instance
* @memberof Chart
* @example
* // When subchart is hidden, will be shown
* // When subchart is shown, will be hidden
* chart.subchart.toggle();
*/
toggle(): void {
const $$ = this.internal;
const {config} = $$;

this.subchart[config.subchart_show ? "hide" : "show"]();
},

/**
* Reset subchart selection
* @function subchart鈥eset
* @instance
* @memberof Chart
* @example
* // Reset subchart selection
* chart.subchart.reset();
*/
reset(): void {
const $$ = this.internal;
const {brush} = $$;

brush.clear(brush.getSelection());
}
});

export default {
subchart
};
56 changes: 16 additions & 40 deletions src/Chart/api/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,7 @@
*/
import {zoomIdentity as d3ZoomIdentity, zoomTransform as d3ZoomTransform} from "d3-zoom";
import {extend, getMinMax, isDefined, isObject, parseDate} from "../../module/util";

/**
* Check if the given domain is within zoom range
* @param {Array} domain Target domain value
* @param {Array} current Current zoom domain value
* @param {Array} range Zoom range value
* @param {boolean} isInverted Whether the axis is inverted or not
* @returns {boolean}
* @private
*/
function withinRange(
domain: (number|Date)[], current, range: number[], isInverted = false
): boolean {
const [min, max] = range;

return domain.every((v, i) => (
i === 0 ? (
isInverted ? +v <= min : +v >= min
) : (
isInverted ? +v >= max : +v <= max
)
) && !(domain.every((v, i) => v === current[i])));
}
import type {TDomainRange} from "../../ChartInternal/data/IData";

/**
* Zoom by giving x domain range.
Expand All @@ -48,29 +26,28 @@ function withinRange(
* // Get the current zoomed domain range
* chart.zoom();
*/
const zoom = function(domainValue?: (Date|number|string)[]): (Date|number)[]|undefined {
// NOTE: declared funciton assigning to variable to prevent duplicated method generation in JSDoc.
const zoom = function<T = TDomainRange>(domainValue?: T): T | undefined {
const $$ = this.internal;
const {$el, axis, config, org, scale} = $$;
const isRotated = config.axis_rotated;
const isInverted = config.axis_x_inverted;
const isCategorized = axis.isCategorized();
let domain = domainValue;
let domain: any = domainValue;

if (config.zoom_enabled && domain) {
if (config.zoom_enabled && Array.isArray(domain)) {
if (axis.isTimeSeries()) {
domain = domain.map(x => parseDate.bind($$)(x));
}

const isWithinRange = withinRange(
domain as (number|Date)[],
$$.getZoomDomain(true),
$$.getZoomDomain(),
isInverted
const isWithinRange = $$.withinRange(
domain,
$$.getZoomDomain("zoom", true),
$$.getZoomDomain("zoom")
);

if (isWithinRange) {
if (isCategorized) {
domain = domain.map((v, i) => Number(v) + (i === 0 ? 0 : 1));
domain = domain.map((v, i) => Number(v) + (i === 0 ? 0 : 1)) as T;
}

// hide any possible tooltip show before the zoom
Expand Down Expand Up @@ -103,11 +80,10 @@ const zoom = function(domainValue?: (Date|number|string)[]): (Date|number)[]|und
$$.setZoomResetButton();
}
} else {
domain = scale.zoom ?
scale.zoom.domain() : scale.x.orgDomain();
domain = scale.zoom?.domain() ?? scale.x.orgDomain();
}

return domain as (Date|number)[];
return domain;
};

extend(zoom, {
Expand All @@ -129,7 +105,7 @@ extend(zoom, {
* // Disable zooming
* chart.zoom.enable(false);
*/
enable: function(enabled: boolean | "wheel" | "drag" | any): void {
enable(enabled: boolean | "wheel" | "drag" | any): void {
const $$ = this.internal;
const {config} = $$;

Expand Down Expand Up @@ -160,7 +136,7 @@ extend(zoom, {
* // Set maximum range value
* chart.zoom.max(20);
*/
max: function(max?: number): number {
max(max?: number): number {
const $$ = this.internal;
const {config, org: {xDomain}} = $$;

Expand All @@ -182,7 +158,7 @@ extend(zoom, {
* // Set minimum range value
* chart.zoom.min(-1);
*/
min: function(min?: number): number {
min(min?: number): number {
const $$ = this.internal;
const {config, org: {xDomain}} = $$;

Expand Down Expand Up @@ -210,7 +186,7 @@ extend(zoom, {
* max: 100
* });
*/
range: function(range): {min: (number|undefined)[], max: (number|undefined)[]} {
range(range): {min: (number|undefined)[], max: (number|undefined)[]} {
const zoom = this.zoom;

if (isObject(range)) {
Expand Down
5 changes: 4 additions & 1 deletion src/ChartInternal/data/IData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type TDataRow = {
name?: string;
};

export type TDomain = Date | number;
export type TDomainRange = [TDomain, TDomain];

export interface ITreemapData {
name: string;
id?: string; // for compatibility
Expand All @@ -19,7 +22,7 @@ export interface ITreemapData {
}

export interface IDataRow extends TDataRow {
x: number | string | Date;
x: TDomain & string;
}

export interface IDataPoint extends IDataRow {
Expand Down
Loading

0 comments on commit ac9e229

Please sign in to comment.