Plotly IBCS Chart Challenge

The following is a universal prompt to re-create the IBCS 09C chart-type within Plotly Studio.

Create an IBCS-style portfolio scatter chart (IBCS example type 09C): Net sales vs. margin with exactly 3 iso-gross-profit curves and a shaded top gross-profit segment. The chart must keep a clean IBCS look: consistent typography, minimal decoration, semantic colors, axes starting at zero, no gridlines, and a clear message tied to the shaded segment.
Year of data collected = 2025
Current year = 2026

A) DATA INGESTION & NORMALIZATION (MUST NOT FAIL)

Auto-detect delimiter (semicolon/comma/tab/pipe) and decimal style (comma vs dot).

Identify columns by fuzzy matching (case-insensitive):

Product label: contains product|item|name|sku|id|code
Product line/category: contains product line|line|category|segment|group|family|division
Margin (%): contains margin|gm%|gross margin|profit%|contribution%
Net sales: contains net sales|sales|revenue|turnover

Coerce numeric:

Margin: if values mostly in 0–1, multiply by 100.

Sales: keep raw unit but infer a display unit label:
If column name includes mUSD|million|mn → label mUSD and don’t rescale.
Else if typical sales values > 1,000,000 → assume currency, display in m (millions) of that currency.
Else keep as-is and label generically (e.g., “Net sales”).

Clean rows:

Remove rows with missing/invalid margin or sales.
Remove rows where margin ≤ 0 or sales ≤ 0.

Compute:
Gross Profit (GP) = Net Sales × Margin / 100 (same unit as Net Sales)

B) DETERMINE “NICE” GP THRESHOLD AND CURVE LEVELS (UNIVERSAL, DETERMINISTIC)

B1) Choose a “focus threshold” T (the top-segment cutoff)

This must be stable for messy datasets and not driven by max outliers.

Compute:
GP70 = 70th percentile of GP
GP90 = 90th percentile of GP

Define the “nice number” ladder (scale-independent):

Base nice numbers: {0.1, 0.2, 0.3, 0.5, 0.8, 1, 2, 3, 4, 5, 8}
Extended by powers of 10: multiply by 10^k as needed (…, 0.01, 0.02, … 10, 20, 30, 50, 80, 100, …)

Set T as:
T = nearest nice number to GP70

Tie-breaker: choose the smaller nice number (more conservative, more stable)

Guardrails to prevent absurd outcomes:

If T <= 0, set T to the smallest positive nice number present in the ladder for the data scale.
If T > GP90, step down one nice number.
If T < 0.2 * median(GP) and GP spread is normal, step up one nice number.

Result:
T is a readable, “round” threshold that fits the dataset scale (e.g., 0.5, 3, 30, 300).

B2) Set EXACTLY 3 curve levels based on T

Use three iso-profit curves at:
L1 = nice(T/3)
L2 = nice(2T/3)
L3 = T

Where nice(x) snaps x to the nearest nice number on the ladder (same tie-breaker: smaller).

Hard constraints:

Ensure L1 < L2 < L3 (if snapping creates duplicates, move the duplicate one step down/up on the ladder).
Always produce exactly 3 curves.

Important: This means if the dataset’s natural threshold is 3, then curves become 1, 2, 3 (matching the reference).
If a tiny dataset has max GP around 0.5, then curves become something like 0.2, 0.3, 0.5 (still correct and readable).

C) BUILD ISO-PROFIT CURVES (SMOOTH HYPERBOLAS)

For each level L in {L1, L2, L3}:

Compute final axis limits first (before generating curves):

X_MAX = nice_round_up(max_margin × 1.1) (this is the actual x-axis upper bound)
Y_MAX = nice_round_up(max_sales × 1.1) (this is the actual y-axis upper bound)

Generate curve x-values to EXACTLY the right-most axis edge:

Create 400 margin values from X_MIN = max(0.5%, 0.7×min_margin) to X_MAX

Compute curve y:
Net Sales = (L × 100) / Margin

Filter points to y ≤ (1.05×y_axis_max) (natural filtering only)

Curve styling:
Line color #BFBFBF
Width 1.5 px
Solid
Behind points

Label each curve at right end with text “L” (format with 0–1 decimals depending on magnitude).

Add a right-side annotation label:
“Gross profit\nin [unit]” (vertical)

Mandatory Curve Labels (Non-Negotiable)

For each iso-profit curve level L in {L1, L2, L3}:

After computing the curve coordinates, take the last visible point of the curve.

Add a separate text-only scatter trace (NOT an annotation) for the label:

mode=“text”
text = formatted value of L
textposition = “middle right”
x = last_margin_value
y = last_sales_value
textfont.size = 10–11
textfont.color = #7F7F7F
showlegend = False

Do NOT use arrows.
Do NOT use layout annotations.

Each iso-profit curve MUST have a visible numeric label at its right end.
Curves without labels are invalid.

D) SHADED ZONE (TOP SEGMENT)

Shade area above the highest curve (L3) across full width:

Fill #EFEFEF
Opacity 40%
Behind curves and points

E) SCATTER POINTS & SEMANTIC COLORS

Scatter:

X = Margin (%)
Y = Net Sales (display unit)

Marker:
filled circle
size ~11
opacity 85%
no outline

Colors:

If product line/category has 3 dominant values, map consistently:
Most frequent → dark gray #4D4D4D
Second → amber #F2C14E
Third → orange #F05A28

If more than 3 categories, color only top 3; group remainder as “Other” in mid gray.

Legend:

Title: “Product lines” (or the detected category name)
Right side, top-aligned

F) AXES, GRID, AND IBCS CLEAN LOOK

Axes must start at 0 (both).

X max:
round up to a “nice” tick (5/10/20 steps) beyond max margin

Y max:
round up to a “nice” tick beyond max sales

Grid:

Light gray gridlines #E6E6E6, thin
Slightly stronger zero-lines

Typography:

Use one sans-serif font family throughout (Arial/Helvetica equivalent)
Limit font sizes (header bigger, body/legend smaller)

Gridlines:

There must be NO gridlines.
Remove all:
• Major gridlines
• Minor gridlines
• Background grids

Keep only clean axes lines.

No:
Gradients
shadows
3D
heavy borders
decorative backgrounds

G) LAYOUT: RESERVE SPACE OUTSIDE THE PLOT (MANDATORY)

The plot (axes + points + curves + shading) must occupy only the inner plotting area.

Reserve dedicated outside space so nothing overlaps the plot:

Top margin ≥ 120 px (for title + insight)
Right margin ≥ 160 px (for legend + “Gross profit …” label)
Bottom margin ≥ 60 px (for footer)

Ensure legend and all text blocks are outside the plotting area.

Implementation requirement (critical):

All header/footer blocks MUST be implemented as layout annotations positioned in paper coordinates, not as chart title and not as data-coordinate annotations.

Use xref=“paper” and yref=“paper” for all header/footer text.

Use y > 1.0 for header content (in the top margin), and y < 0 for footer content (in the bottom margin).

H) HEADER TITLE BLOCK (TOP-LEFT, OUTSIDE PLOT)

Create three separate text lines as paper-anchored annotations placed above the plotting area:

Line 1 (bold): Alpha Corp., Paper division
Line 2 (regular): Net sales in [unit], margin in %
Line 3 (regular): [Year]

Absolute positioning rules (paper coords):

x = 0.00 (left edge of plotting paper)
y = 1.18 for Line 1
y = 1.12 for Line 2
y = 1.06 for Line 3

left aligned:
xanchor=“left”
align=“left”

Font family:
one sans-serif everywhere (Arial/Helvetica)

Sizes:
14 (bold)
12
12

Color:
#333333

No arrows:
showarrow=False

Non-negotiable:
Do NOT use layout.title for this.
Use annotations only.

I) INSIGHT STATEMENT (OUTSIDE PLOT, UNDER TITLE)

Compute:

COUNT = number of products with GP >= L3
DOMINANT_LINE = most frequent product line among those

If tie:
omit DOMINANT_LINE clause

Create the insight sentence (exact wording):

In [Year] we had [COUNT] products of the product line [DOMINANT_LINE] in the gross profit segment of [L3] [unit] and above

Place it as a paper-anchored annotation:

x = 0.32 (to appear centered-ish like the reference header row)
y = 1.12
xref=“paper”
yref=“paper”
xanchor=“left”
align=“left”

Font:
12
bold
color #666666

showarrow=False

Non-negotiable:
It must be outside the plot area (y > 1.0), never inside.

J) LEGEND + RIGHT-SIDE “GROSS PROFIT” LABEL (OUTSIDE PLOT)

Legend on the right, top-aligned, outside plot.

Add a vertical text block on the far right (paper-anchored), outside plot:

Annotation text:
Gross profit
in [unit]

Position:
x = 1.08
y = 0.50
xref=“paper”
yref=“paper”

Rotate -90

Font 11
color #666666

showarrow=False

K) FOOTER (OUTSIDE PLOT)

Two paper-anchored annotations below the plot (y < 0):

Bottom-left date:
x=0.00
y=-0.18
xanchor=“left”

Bottom-right:
“© [Current year] IBCS Institute | ``www.ibcs.com``”
x=1.00
y=-0.18
xanchor=“right”

Font 9
color #9A9A9A

showarrow=False

Fix marker labels so there are NO ARROWS / NO LEADER LINES

L) PRODUCT LABELS MUST BE TRACE TEXT, NOT ANNOTATIONS

  1. Label 3–5 highest GP products.
  2. Label at least 1 product near the L3 threshold.
  3. Label highest margin product.
  4. Label highest sales product.
  5. Ensure each dominant product line appears at least once.
  6. Cap total labelled products at 12.
  7. Ensure 8-12 products are labelled.

For these selected labelled products, display labels as marker text (trace text), not layout annotations.

Rules:

Use scatter trace with mode=“markers+text”

Set:
textposition=“top right”
textfont.size=10
textfont.family same as chart
textfont.color = category color

Disable arrows completely:

Do NOT create any annotation with showarrow=True
Do NOT use “callouts”
Do NOT draw leader lines

If overlap occurs:
Prefer hiding some labels (keep the top GP ones) rather than adding arrows/lines.

Non-negotiable:
Labels must be plain text near the points only.

IMPORTANT:
Every instruction above is mandatory and non-optional.

Data visual created in Plotly Studio with above prompt:

Original data visual:

2 Likes