Skip to content

Commit

Permalink
feat(funnel): Intent to ship funnel type
Browse files Browse the repository at this point in the history
Implement new funnel type

Close #3449
  • Loading branch information
netil committed May 21, 2024
1 parent 1693d50 commit e4cdda1
Show file tree
Hide file tree
Showing 37 changed files with 934 additions and 39 deletions.
2 changes: 1 addition & 1 deletion demo/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ var billboardDemo = {

// UMD
code.data = code.data.join("")
.replace(/"(area|area-line-range|area-spline|area-spline-range|area-step|bar|bubble|candlestick|donut|gauge|line|pie|polar|radar|scatter|spline|step|treemap|selection|subchart|zoom)(\(\))?",?/g, function(match, p1, p2, p3, offset, string) {
.replace(/"(area|area-line-range|area-spline|area-spline-range|area-step|bar|bubble|candlestick|donut|funnel|gauge|line|pie|polar|radar|scatter|spline|step|treemap|selection|subchart|zoom)(\(\))?",?/g, function(match, p1, p2, p3, offset, string) {
var module = camelize(p1);

code.esm.indexOf(module) === -1 &&
Expand Down
75 changes: 74 additions & 1 deletion demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,79 @@ var demos = {
];
}
},
FunnelChart: [
{
options: {
data: {
columns: [
["data1", 30],
["data2", 45],
["data3", 25],
["data4", 55]
],
type: "funnel",
order: null,
labels: {
format: function(v, id, i, texts) {
return id;
}
}
}
}
},
{
options: {
data: {
columns: [
["data1", 11300],
["data2", 12245],
["data3", 11125],
["data4", 13355],
["data5", 18562]
],
type: "funnel",
labels: true,
order: "asc"
},
funnel: {
neck: {
width: 200,
height: 50
}
}
}
},
{
options: {
data: {
columns: [
["data1", 130],
["data2", 245],
["data3", 425],
["data4", 325],
["data5", 555]
],
type: "funnel",
order: "desc",
labels: {
format: function(v, id, i, texts) {
return `${id} ${v}`;
}
}
},
funnel: {
neck: {
width: {
ratio: 0.3
},
height: {
ratio: 0.5
}
}
}
}
},
],
GaugeChart: {
options: {
data: {
Expand Down Expand Up @@ -5621,7 +5694,7 @@ setTimeout(function() {
]
}
],
DonutRangeText: [{
DonutRangeText: [{
options: {
title: {
text: "Range text in 'absolute' value"
Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ <h4>Supported chart types</h4>
<li><a href="#Chart.CandlestickChart">Candlestick</a></li>
<li><a href="#Chart.CombinationChart">Combination</a></li>
<li><a href="#Chart.DonutChart">Donut</a></li>
<li><a href="#Chart.FunnelChart">Funnel</a></li>
<li><a href="#Chart.GaugeChart">Gauge</a></li>
<li><a href="#Chart.LineChart">Line</a></li>
<li><a href="#Chart.PieChart">Pie</a></li>
Expand Down
11 changes: 6 additions & 5 deletions src/Chart/api/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {$SHAPE} from "../../config/classes";
import {isDefined} from "../../module/util";

/**
Expand Down Expand Up @@ -72,7 +73,7 @@ const tooltip = {
*/
show: function(args): void {
const $$ = this.internal;
const {$el, config, state: {eventReceiver, hasTreemap, inputType}} = $$;
const {$el, config, state: {eventReceiver, hasFunnel, hasTreemap, inputType}} = $$;
let index;
let mouse;

Expand All @@ -86,10 +87,10 @@ const tooltip = {
const {data} = args;
const y = $$.getYScaleById(data.id)?.(data.value);

if (hasTreemap && data.id) {
eventReceiver.rect = $el.main.select(
`${$$.selectorTarget(data.id, undefined, "rect")}`
);
if ((hasFunnel || hasTreemap) && data.id) {
const selector = $$.selectorTarget(data.id, undefined, `.${$SHAPE.shape}`);

eventReceiver.rect = $el.main.select(selector);
} else if ($$.isMultipleX()) {
// if multiple xs, target point will be determined by mouse
mouse = [$$.xx(data), y];
Expand Down
14 changes: 9 additions & 5 deletions src/ChartInternal/ChartInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,9 @@ export default class ChartInternal {
checkModuleImport($$);

state.hasRadar = !state.hasAxis && $$.hasType("radar");
state.hasFunnel = !state.hasAxis && $$.hasType("funnel");
state.hasTreemap = !state.hasAxis && $$.hasType("treemap");
state.hasAxis = !$$.hasArcType() && !state.hasTreemap;
state.hasAxis = !$$.hasArcType() && !state.hasFunnel && !state.hasTreemap;

// datetime to be used for uniqueness
state.datetimeId = `bb-${+new Date() * (getRandom() as number)}`;
Expand Down Expand Up @@ -347,7 +348,7 @@ export default class ChartInternal {
initWithData(data): void {
const $$ = <any>this;
const {config, scale, state, $el, org} = $$;
const {hasAxis, hasTreemap} = state;
const {hasAxis, hasFunnel, hasTreemap} = state;
const hasInteraction = config.interaction_enabled;
const hasPolar = $$.hasType("polar");
const labelsBGColor = config.data_labels_backgroundColors;
Expand Down Expand Up @@ -458,7 +459,7 @@ export default class ChartInternal {
// Define regions
const main = $el.svg.append("g")
.classed($COMMON.main, true)
.attr("transform", hasTreemap ? null : $$.getTranslate("main"));
.attr("transform", hasFunnel || hasTreemap ? null : $$.getTranslate("main"));

$el.main = main;

Expand Down Expand Up @@ -566,6 +567,8 @@ export default class ChartInternal {
});
} else if (hasTreemap) {
types.push("Treemap");
} else if ($$.hasType("funnel")) {
types.push("Funnel");
} else {
const hasPolar = $$.hasType("polar");

Expand Down Expand Up @@ -670,7 +673,7 @@ export default class ChartInternal {
*/
updateTargets(targets): void {
const $$ = <any>this;
const {hasAxis, hasRadar, hasTreemap} = $$.state;
const {hasAxis, hasFunnel, hasRadar, hasTreemap} = $$.state;
const helper = type =>
$$[`updateTargetsFor${type}`](
targets.filter($$[`is${type}Type`].bind($$))
Expand Down Expand Up @@ -703,7 +706,8 @@ export default class ChartInternal {
}

helper(type);
// Arc, Polar, Radar
} else if (hasFunnel) {
helper("Funnel");
} else if (hasTreemap) {
helper("Treemap");
}
Expand Down
7 changes: 7 additions & 0 deletions src/ChartInternal/data/IData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ type TDataRow = {value: number | null, id: string, index: number, name?: string}
export type TDomain = Date | number;
export type TDomainRange = [TDomain, TDomain];

export interface IFunnelData {
id: string; // for compatibility
value: number;
ratio?: number;
coords?: number[][];
}

export interface ITreemapData {
name: string;
id?: string; // for compatibility
Expand Down
26 changes: 16 additions & 10 deletions src/ChartInternal/interactions/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,18 @@ export default {
*/
setOverOut(isOver: boolean, d: number | IArcDataRow): void {
const $$ = this;
const {config, state: {hasRadar, hasTreemap}, $el: {main}} = $$;
const isArcTreemap = isObject(d);
const {config, state: {hasFunnel, hasRadar, hasTreemap}, $el: {main}} = $$;
const isArcishData = isObject(d);

// Call event handler
if (isArcTreemap || d !== -1) {
if (isArcishData || d !== -1) {
const callback = config[isOver ? "data_onover" : "data_onout"].bind($$.api);

config.color_onover && $$.setOverColor(isOver, d, isArcTreemap);
config.color_onover && $$.setOverColor(isOver, d, isArcishData);

if (isArcTreemap && "id") {
if (isArcishData && "id") {
const suffix = $$.getTargetSelectorSuffix((d as IArcDataRow).id);
const selector = hasTreemap ?
const selector = hasFunnel || hasTreemap ?
`${$COMMON.target + suffix} .${$SHAPE.shape}` :
$ARC.arc + suffix;

Expand Down Expand Up @@ -179,13 +179,14 @@ export default {
state: {
eventReceiver,
hasAxis,
hasFunnel,
hasRadar,
hasTreemap
},
$el: {eventRect, radar, treemap}
$el: {eventRect, funnel, radar, treemap}
} = $$;
const element = (
(hasTreemap && eventReceiver.rect) ||
let element = (
((hasFunnel || hasTreemap) && eventReceiver.rect) ||
(hasRadar && radar.axes.select(`.${$AXIS.axis}-${index} text`)) || (
eventRect || $$.getArcElementByIdOrIndex?.(index)
)
Expand Down Expand Up @@ -224,8 +225,13 @@ export default {
bubbles: hasRadar // radar type needs to bubble up event
};

// for funnel and treemap event bound to <g> node
if (hasFunnel || hasTreemap) {
element = (funnel ?? treemap).node();
}

emulateEvent[/^(mouse|click)/.test(type) ? "mouse" : "touch"](
hasTreemap ? treemap.node() : element,
element,
type,
params
);
Expand Down
2 changes: 1 addition & 1 deletion src/ChartInternal/internals/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default {
*/
getClass(type: string, withShape: boolean): Function {
const isPlural = /s$/.test(type);
const useIdKey = /^(area|arc|line|treemap)s?$/.test(type);
const useIdKey = /^(area|arc|line|funnel|treemap)s?$/.test(type);
const key = isPlural ? "id" : "index";

return (d): string => {
Expand Down
3 changes: 3 additions & 0 deletions src/ChartInternal/internals/redraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export default {
// polar
$el.polar && $$.redrawPolar();

// funnel
$el.funnel && $$.redrawFunnel();

// treemap
treemap && $$.updateTreemap(durationForExit);
}
Expand Down
2 changes: 1 addition & 1 deletion src/ChartInternal/internals/size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export default {
const $$ = this;
const {config, state, $el: {legend}} = $$;
const isRotated = config.axis_rotated;
const isNonAxis = $$.hasArcType() || state.hasTreemap;
const isNonAxis = $$.hasArcType() || state.hasFunnel || state.hasTreemap;
const isFitPadding = config.padding?.mode === "fit";

!isInit && $$.setContainerSize();
Expand Down
23 changes: 17 additions & 6 deletions src/ChartInternal/internals/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default {

$el.main.select(`.${$COMMON.chart}`).append("g")
.attr("class", $TEXT.chartTexts)
.style("pointer-events", $el.treemap ? "none" : null);
.style("pointer-events", $el.funnel || $el.treemap ? "none" : null);
},

/**
Expand Down Expand Up @@ -240,9 +240,10 @@ export default {
const $$ = this;
const {config} = $$;
const labelColors = config.data_labels_colors;
const defaultColor = ($$.isArcType(d) && !$$.isRadarType(d)) || $$.isTreemapType(d) ?
null :
$$.color(d);
const defaultColor =
($$.isArcType(d) && !$$.isRadarType(d)) || $$.isFunnelType(d) || $$.isTreemapType(d) ?
null :
$$.color(d);
let color;

if (isString(labelColors)) {
Expand Down Expand Up @@ -390,11 +391,12 @@ export default {
*/
generateXYForText(indices, forX?: boolean): (d, i) => number {
const $$ = this;
const {state: {hasRadar, hasTreemap}} = $$;
const {state: {hasRadar, hasFunnel, hasTreemap}} = $$;
const types = Object.keys(indices);
const points = {};
const getter = forX ? $$.getXForText : $$.getYForText;

hasFunnel && types.push("funnel");
hasRadar && types.push("radar");
hasTreemap && types.push("treemap");

Expand All @@ -406,6 +408,7 @@ export default {
const type = ($$.isAreaType(d) && "area") ||
($$.isBarType(d) && "bar") ||
($$.isCandlestickType(d) && "candlestick") ||
($$.isFunnelType(d) && "funnel") ||
($$.isRadarType(d) && "radar") ||
($$.isTreemapType(d) && "treemap") || "line";

Expand Down Expand Up @@ -474,15 +477,18 @@ export default {
const $$ = this;
const {config} = $$;
const isRotated = config.axis_rotated;
const isFunnelType = $$.isFunnelType(d);
const isTreemapType = $$.isTreemapType(d);
let xPos = points[0][0];
let xPos = points ? points[0][0] : 0;

if ($$.isCandlestickType(d)) {
if (isRotated) {
xPos = $$.getCandlestickData(d)?._isUp ? points[2][2] + 4 : points[2][1] - 4;
} else {
xPos += (points[1][0] - xPos) / 2;
}
} else if (isFunnelType) {
xPos += $$.state.current.width / 2;
} else if (isTreemapType) {
xPos += config.data_labels.centered ? 0 : 5;
} else {
Expand Down Expand Up @@ -524,6 +530,7 @@ export default {
const isRotated = config.axis_rotated;
const isInverted = config[`axis_${axis?.getId(d.id)}_inverted`];
const isBarType = $$.isBarType(d);
const isFunnelType = $$.isFunnelType(d);
const isTreemapType = $$.isTreemapType(d);
const r = config.point_r;
const rect = getBoundingRect(textElement);
Expand All @@ -544,6 +551,10 @@ export default {
yPos += 15 * (value._isUp ? 1 : -1);
}
}
} else if (isFunnelType) {
yPos = points ?
points[0][1] + ((points[1][1] - points[0][1]) / 2) + rect.height / 2 - 3 :
0;
} else if (isTreemapType) {
yPos = points[0][1] + (config.data_labels.centered ? 0 : rect.height + 5);
} else {
Expand Down
Loading

0 comments on commit e4cdda1

Please sign in to comment.