Context: Searching for a new senior level software development job over a 9 week period in summer 2025.
- Focused mostly on data engineering and backend roles that are in-person or hybrid in the SF Bay Area.
- Leads from recruiters on LinkedIn were much more likely to lead to interviews+offers.
- The winning offer came through my personal network.
- I mostly used Hiring.cafe for prospecting. They’re a scraper with an interface I didn’t hate.
The better option is to keep colors from the original input stream for the flows instead of making the flows an uniform color.
In the input on the left you have pink, green and blue.
Keep these colors throughout the graph.
Except of the input, all of the other stages only ever split up and never merge, so keeping this single set of colors is enough.
The other option would be to get rid of the “leads” stage, since it actually doesn’t change any state. All the other stages are an action that happens (e.g. “Applied” changes the state of the application from being just a lead to being an open application and it also filters out data for being e.g. abandoned). But the “leads” stage means the same thing as the first stage. So drop the “leads” stage and instead make flows go from all three input stages directly into “bad lead”, “abandoned” or “applied”.
Combine both to get the best result.
Is there any site that does this?
Don’t know if there’s a ready-made site for stuff like that, but it’s not hard to do.
Here’s a quick and dirty AI generated piece of trash code as a proof of concept:
# sankey_hiring_funnel_direct.py # Requires: plotly # Install: pip install plotly import plotly.graph_objects as go # Node labels (unique) labels = [ "Network", # 0 "Hiring.cafe", # 1 "Abandoned Lead", # 2 "Applied", # 3 "Rejected", # 4 "No Response", # 5 "Screener", # 6 "Rejected by Screen", # 7 "Full Round", # 8 "Rejected by Panel", # 9 "Offer", #10 "Accepted", #11 "Declined" #12 ] # Colors for the two source groups (consistent) network_color = "rgba(31,119,180,0.8)" # blue-ish hiring_color = "rgba(255,127,14,0.8)" # orange-ish sources = [] targets = [] values = [] link_colors = [] def add_link(src_idx, tgt_idx, val, color): sources.append(src_idx) targets.append(tgt_idx) values.append(val) link_colors.append(color) # Direct flows from Network and Hiring.cafe into Abandoned Lead and Applied add_link(0, 2, 1, network_color) # Network -> Abandoned Lead (1) add_link(1, 2, 58, hiring_color) # Hiring.cafe -> Abandoned Lead (58) add_link(0, 3, 11, network_color) # Network -> Applied (11) add_link(1, 3, 70, hiring_color) # Hiring.cafe -> Applied (70) # Applied -> Rejected, No Response, Screener (split by original group) add_link(3, 4, 5, network_color) # Applied -> Rejected (network 5) add_link(3, 4, 40, hiring_color) # Applied -> Rejected (hiring 40) add_link(3, 5, 3, network_color) # Applied -> No Response (network 3) add_link(3, 5, 15, hiring_color) # Applied -> No Response (hiring 15) add_link(3, 6, 4, network_color) # Applied -> Screener (network 4) add_link(3, 6, 15, hiring_color) # Applied -> Screener (hiring 15) # Screener -> Rejected by Screen, Full Round add_link(6, 7, 1, network_color) # Screener -> Rejected by Screen (network 1) add_link(6, 7, 5, hiring_color) # Screener -> Rejected by Screen (hiring 5) add_link(6, 8, 3, network_color) # Screener -> Full Round (network 3) add_link(6, 8, 10, hiring_color) # Screener -> Full Round (hiring 10) # Full Round -> Rejected by Panel, Offer add_link(8, 9, 1, network_color) # Full Round -> Rejected by Panel (network 1) add_link(8, 9, 7, hiring_color) # Full Round -> Rejected by Panel (hiring 7) add_link(8, 10, 2, network_color) # Full Round -> Offer (network 2) add_link(8, 10, 3, hiring_color) # Full Round -> Offer (hiring 3) # Offer -> Accepted, Declined add_link(10, 11, 1, network_color) # Offer -> Accepted (network 1) add_link(10, 12, 1, network_color) # Offer -> Declined (network 1) add_link(10, 12, 3, hiring_color) # Offer -> Declined (hiring 3) # Sanity check assert len(sources) == len(targets) == len(values) == len(link_colors) # Node colors (visual guidance) node_colors = [ "rgba(31,119,180,0.9)", # Network "rgba(255,127,14,0.9)", # Hiring.cafe "rgba(220,220,220,0.9)", # Abandoned Lead "rgba(200,200,200,0.9)", # Applied "rgba(220,180,180,0.9)", # Rejected "rgba(200,200,220,0.9)", # No Response "rgba(200,220,200,0.9)", # Screener "rgba(255,200,200,0.9)", # Rejected by Screen "rgba(210,210,255,0.9)", # Full Round "rgba(240,200,220,0.9)", # Rejected by Panel "rgba(200,255,200,0.9)", # Offer "rgba(140,255,140,0.9)", # Accepted "rgba(255,140,140,0.9)" # Declined ] fig = go.Figure(data=[go.Sankey( node=dict( pad=18, thickness=18, line=dict(color="black", width=0.5), label=labels, color=node_colors ), link=dict( source=sources, target=targets, value=values, color=link_colors, hovertemplate='%{source.label} → %{target.label}: %{value}<extra></extra>' ) )]) fig.update_layout( title_text="Hiring funnel Sankey — direct source flows (no Leads node)", font_size=12, height=700, margin=dict(l=20, r=20, t=60, b=20) ) fig.show() # To save as interactive HTML: # fig.write_html("sankey_hiring_funnel_direct.html", include_plotlyjs='cdn')
Couldn’t be bothered to write this by hand for just an online comment. There’s enough that can be improved with this, but I think it’s ok to show how it can be done quite easily.
Thanks for sharing that. Seems like a promising vis technique but would work better with fewer final states than I used for a regular Sankey.
I’m sure it’s possible to move the final states to the middle positions like you did. But I didn’t want to invest more time.