• Home
  • About Us
  • Contact Us
  • Disclaimer
  • Privacy Policy
Thursday, July 3, 2025
newsaiworld
  • Home
  • Artificial Intelligence
  • ChatGPT
  • Data Science
  • Machine Learning
  • Crypto Coins
  • Contact Us
No Result
View All Result
  • Home
  • Artificial Intelligence
  • ChatGPT
  • Data Science
  • Machine Learning
  • Crypto Coins
  • Contact Us
No Result
View All Result
Morning News
No Result
View All Result
Home Artificial Intelligence

4-Dimensional Knowledge Visualization: Time in Bubble Charts

Admin by Admin
February 12, 2025
in Artificial Intelligence
0
Zhyvov 1.png
0
SHARES
0
VIEWS
Share on FacebookShare on Twitter

READ ALSO

Learn how to Maximize Technical Occasions — NVIDIA GTC Paris 2025

Find out how to Entry NASA’s Local weather Information — And How It’s Powering the Struggle Towards Local weather Change Pt. 1


Bubble Charts elegantly compress giant quantities of data right into a single visualization, with bubble measurement including a 3rd dimension. Nevertheless, evaluating “earlier than” and “after” states is usually essential. To deal with this, we suggest including a transition between these states, creating an intuitive consumer expertise.

Since we couldn’t discover a ready-made answer, we developed our personal. The problem turned out to be fascinating and required refreshing some mathematical ideas.

For sure, probably the most difficult a part of the visualization is the transition between two circles — earlier than and after states. To simplify, we give attention to fixing a single case, which might then be prolonged in a loop to generate the mandatory variety of transitions.

Base factor, picture by Creator

To construct such a determine, let’s first decompose it into three elements: two circles and a polygon that connects them (in grey).

Base factor decomposition, picture by Creator

Constructing two circles is sort of easy — we all know their facilities and radii. The remaining activity is to assemble a quadrilateral polygon, which has the next kind:

Polygon, picture by Creator

The development of this polygon reduces to discovering the coordinates of its vertices. That is probably the most fascinating activity, and we’ll clear up it additional.

From polygon to tangent traces, picture by Creator

To calculate the space from some extent (x1, y1) to the road ax+y+b=0, the formulation is:

Distance from level to a line, picture by Creator

In our case, distance (d) is the same as circle radius (r). Therefore,

Distance to radius, picture by Creator

After multiplying either side of the equation by a**2+1, we get:

Base math, picture by Creator

After shifting all the things to at least one facet and setting the equation equal to zero, we get:

Base math, picture by Creator

Since we’ve two circles and must discover a tangent to each, we’ve the next system of equations:

System of equations, picture by Creator

This works nice, however the issue is that we’ve 4 potential tangent traces in actuality:

All potential tangent traces, picture by Creator

And we have to select simply 2 of them — exterior ones.

To do that we have to test every tangent and every circle middle and decide if the road is above or under the purpose:

Verify if line is above or under the purpose, picture by Creator

We’d like the 2 traces that each go above or each go under the facilities of the circles.

Now, let’s translate all these steps into code:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sympy as sp
from scipy.spatial import ConvexHull
import math
from matplotlib import rcParams
import matplotlib.patches as patches

def check_position_relative_to_line(a, b, x0, y0):
    y_line = a * x0 + b
    
    if y0 > y_line:
        return 1 # line is above the purpose
    elif y0 < y_line:
        return -1

    
def find_tangent_equations(x1, y1, r1, x2, y2, r2):
    a, b = sp.symbols('a b')

    tangent_1 = (a*x1 + b - y1)**2 - r1**2 * (a**2 + 1)  
    tangent_2 = (a*x2 + b - y2)**2 - r2**2 * (a**2 + 1) 

    eqs_1 = [tangent_2, tangent_1]
    answer = sp.clear up(eqs_1, (a, b))
    parameters = [(float(e[0]), float(e[1])) for e in answer]

    # filter simply exterior tangents
    parameters_filtered = []
    for tangent in parameters:
        a = tangent[0]
        b = tangent[1]
        if abs(check_position_relative_to_line(a, b, x1, y1) + check_position_relative_to_line(a, b, x2, y2)) == 2:
            parameters_filtered.append(tangent)

    return parameters_filtered

Now, we simply want to seek out the intersections of the tangents with the circles. These 4 factors would be the vertices of the specified polygon.

Circle equation:

Circle equation, picture by Creator

Substitute the road equation y=ax+b into the circle equation:

Base math, picture by Creator

Answer of the equation is the x of the intersection.

Then, calculate y from the road equation:

Calculating y, picture by Creator

The way it interprets to the code:

def find_circle_line_intersection(circle_x, circle_y, circle_r, line_a, line_b):
    x, y = sp.symbols('x y')
    circle_eq = (x - circle_x)**2 + (y - circle_y)**2 - circle_r**2
    intersection_eq = circle_eq.subs(y, line_a * x + line_b)

    sol_x_raw = sp.clear up(intersection_eq, x)[0]
    attempt:
        sol_x = float(sol_x_raw)
    besides:
        sol_x = sol_x_raw.as_real_imag()[0]
    sol_y = line_a * sol_x + line_b
    return sol_x, sol_y

Now we need to generate pattern information to show the entire chart compositions.

Think about we’ve 4 customers on our platform. We all know what number of purchases they made, generated income and exercise on the platform. All these metrics are calculated for two intervals (let’s name them pre and put up interval).

# information technology
df = pd.DataFrame({'consumer': ['Emily', 'Emily', 'James', 'James', 'Tony', 'Tony', 'Olivia', 'Olivia'],
                   'interval': ['pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post'],
                   'num_purchases': [10, 9, 3, 5, 2, 4, 8, 7],
                   'income': [70, 60, 80, 90, 20, 15, 80, 76],
                   'exercise': [100, 80, 50, 90, 210, 170, 60, 55]})
Knowledge pattern, picture by Creator

Let’s assume that “exercise” is the realm of the bubble. Now, let’s convert it into the radius of the bubble. We will even scale the y-axis.

def area_to_radius(space):
    radius = math.sqrt(space / math.pi)
    return radius

x_alias, y_alias, a_alias="num_purchases", 'income', 'exercise'

# scaling metrics
radius_scaler = 0.1
df['radius'] = df[a_alias].apply(area_to_radius) * radius_scaler
df['y_scaled'] = df[y_alias] / df[x_alias].max()

Now let’s construct the chart — 2 circles and the polygon.

def draw_polygon(plt, factors):
    hull = ConvexHull(factors)
    convex_points = [points[i] for i in hull.vertices]

    x, y = zip(*convex_points)
    x += (x[0],)
    y += (y[0],)

    plt.fill(x, y, colour="#99d8e1", alpha=1, zorder=1)

# bubble pre
for _, row in df[df.period=='pre'].iterrows():
    x = row[x_alias]
    y = row.y_scaled
    r = row.radius
    circle = patches.Circle((x, y), r, facecolor="#99d8e1", edgecolor="none", linewidth=0, zorder=2)
    plt.gca().add_patch(circle)

# transition space
for consumer in df.consumer.distinctive():
    user_pre = df[(df.user==user) & (df.period=='pre')]
    x1, y1, r1 = user_pre[x_alias].values[0], user_pre.y_scaled.values[0], user_pre.radius.values[0]
    user_post = df[(df.user==user) & (df.period=='post')]
    x2, y2, r2 = user_post[x_alias].values[0], user_post.y_scaled.values[0], user_post.radius.values[0]

    tangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)
    circle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]
    circle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]

    polygon_points = circle_1_line_intersections + circle_2_line_intersections
    draw_polygon(plt, polygon_points)

# bubble put up
for _, row in df[df.period=='post'].iterrows():
    x = row[x_alias]
    y = row.y_scaled
    r = row.radius
    label = row.consumer
    circle = patches.Circle((x, y), r, facecolor="#2d699f", edgecolor="none", linewidth=0, zorder=2)
    plt.gca().add_patch(circle)

    plt.textual content(x, y - r - 0.3, label, fontsize=12, ha="middle")

The output seems as anticipated:

Output, picture by Creator

Now we need to add some styling:

# plot parameters
plt.subplots(figsize=(10, 10))
rcParams['font.family'] = 'DejaVu Sans'
rcParams['font.size'] = 14
plt.grid(colour="grey", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6, zorder=1)
plt.axvline(x=0, colour="white", linewidth=2)
plt.gca().set_facecolor('white')
plt.gcf().set_facecolor('white')

# spines formatting
plt.gca().spines["top"].set_visible(False)
plt.gca().spines["right"].set_visible(False)
plt.gca().spines["bottom"].set_visible(False)
plt.gca().spines["left"].set_visible(False)
plt.gca().tick_params(axis="each", which="each", size=0)

# plot labels
plt.xlabel("Quantity purchases") 
plt.ylabel("Income, $")
plt.title("Product customers efficiency", fontsize=18, colour="black")

# axis limits
axis_lim = df[x_alias].max() * 1.2
plt.xlim(0, axis_lim)
plt.ylim(0, axis_lim)

Pre-post legend in the appropriate backside nook to offer viewer a touch, methods to learn the chart:

## pre-post legend 
# circle 1
legend_position, r1 = (11, 2.2), 0.3
x1, y1 = legend_position[0], legend_position[1]
circle = patches.Circle((x1, y1), r1, facecolor="#99d8e1", edgecolor="none", linewidth=0, zorder=2)
plt.gca().add_patch(circle)
plt.textual content(x1, y1 + r1 + 0.15, 'Pre', fontsize=12, ha="middle", va="middle")
# circle 2
x2, y2 = legend_position[0], legend_position[1] - r1*3
r2 = r1*0.7
circle = patches.Circle((x2, y2), r2, facecolor="#2d699f", edgecolor="none", linewidth=0, zorder=2)
plt.gca().add_patch(circle)
plt.textual content(x2, y2 - r2 - 0.15, 'Submit', fontsize=12, ha="middle", va="middle")
# tangents
tangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)
circle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]
circle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]
polygon_points = circle_1_line_intersections + circle_2_line_intersections
draw_polygon(plt, polygon_points)
# small arrow
plt.annotate('', xytext=(x1, y1), xy=(x2, y1 - r1*2), arrowprops=dict(edgecolor="black", arrowstyle="->", lw=1))
Including styling and legend, picture by Creator

And at last bubble-size legend:

# bubble measurement legend
legend_areas_original = [150, 50]
legend_position = (11, 10.2)
for i in legend_areas_original:
    i_r = area_to_radius(i) * radius_scaler
    circle = plt.Circle((legend_position[0], legend_position[1] + i_r), i_r, colour="black", fill=False, linewidth=0.6, facecolor="none")
    plt.gca().add_patch(circle)
    plt.textual content(legend_position[0], legend_position[1] + 2*i_r, str(i), fontsize=12, ha="middle", va="middle",
              bbox=dict(facecolor="white", edgecolor="none", boxstyle="spherical,pad=0.1"))
legend_label_r = area_to_radius(np.max(legend_areas_original)) * radius_scaler
plt.textual content(legend_position[0], legend_position[1] + 2*legend_label_r + 0.3, 'Exercise, hours', fontsize=12, ha="middle", va="middle")

Our closing chart seems like this:

Including second legend, picture by Creator

The visualization seems very fashionable and concentrates numerous data in a compact kind.

Right here is the complete code for the graph:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sympy as sp
from scipy.spatial import ConvexHull
import math
from matplotlib import rcParams
import matplotlib.patches as patches

def check_position_relative_to_line(a, b, x0, y0):
    y_line = a * x0 + b
    
    if y0 > y_line:
        return 1 # line is above the purpose
    elif y0 < y_line:
        return -1

    
def find_tangent_equations(x1, y1, r1, x2, y2, r2):
    a, b = sp.symbols('a b')

    tangent_1 = (a*x1 + b - y1)**2 - r1**2 * (a**2 + 1)  
    tangent_2 = (a*x2 + b - y2)**2 - r2**2 * (a**2 + 1) 

    eqs_1 = [tangent_2, tangent_1]
    answer = sp.clear up(eqs_1, (a, b))
    parameters = [(float(e[0]), float(e[1])) for e in answer]

    # filter simply exterior tangents
    parameters_filtered = []
    for tangent in parameters:
        a = tangent[0]
        b = tangent[1]
        if abs(check_position_relative_to_line(a, b, x1, y1) + check_position_relative_to_line(a, b, x2, y2)) == 2:
            parameters_filtered.append(tangent)

    return parameters_filtered

def find_circle_line_intersection(circle_x, circle_y, circle_r, line_a, line_b):
    x, y = sp.symbols('x y')
    circle_eq = (x - circle_x)**2 + (y - circle_y)**2 - circle_r**2
    intersection_eq = circle_eq.subs(y, line_a * x + line_b)

    sol_x_raw = sp.clear up(intersection_eq, x)[0]
    attempt:
        sol_x = float(sol_x_raw)
    besides:
        sol_x = sol_x_raw.as_real_imag()[0]
    sol_y = line_a * sol_x + line_b
    return sol_x, sol_y

def draw_polygon(plt, factors):
    hull = ConvexHull(factors)
    convex_points = [points[i] for i in hull.vertices]

    x, y = zip(*convex_points)
    x += (x[0],)
    y += (y[0],)

    plt.fill(x, y, colour="#99d8e1", alpha=1, zorder=1)

def area_to_radius(space):
    radius = math.sqrt(space / math.pi)
    return radius

# information technology
df = pd.DataFrame({'consumer': ['Emily', 'Emily', 'James', 'James', 'Tony', 'Tony', 'Olivia', 'Olivia', 'Oliver', 'Oliver', 'Benjamin', 'Benjamin'],
                   'interval': ['pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post', 'pre', 'post'],
                   'num_purchases': [10, 9, 3, 5, 2, 4, 8, 7, 6, 7, 4, 6],
                   'income': [70, 60, 80, 90, 20, 15, 80, 76, 17, 19, 45, 55],
                   'exercise': [100, 80, 50, 90, 210, 170, 60, 55, 30, 20, 200, 120]})

x_alias, y_alias, a_alias="num_purchases", 'income', 'exercise'

# scaling metrics
radius_scaler = 0.1
df['radius'] = df[a_alias].apply(area_to_radius) * radius_scaler
df['y_scaled'] = df[y_alias] / df[x_alias].max()

# plot parameters
plt.subplots(figsize=(10, 10))
rcParams['font.family'] = 'DejaVu Sans'
rcParams['font.size'] = 14
plt.grid(colour="grey", linestyle=(0, (10, 10)), linewidth=0.5, alpha=0.6, zorder=1)
plt.axvline(x=0, colour="white", linewidth=2)
plt.gca().set_facecolor('white')
plt.gcf().set_facecolor('white')

# spines formatting
plt.gca().spines["top"].set_visible(False)
plt.gca().spines["right"].set_visible(False)
plt.gca().spines["bottom"].set_visible(False)
plt.gca().spines["left"].set_visible(False)
plt.gca().tick_params(axis="each", which="each", size=0)

# plot labels
plt.xlabel("Quantity purchases") 
plt.ylabel("Income, $")
plt.title("Product customers efficiency", fontsize=18, colour="black")

# axis limits
axis_lim = df[x_alias].max() * 1.2
plt.xlim(0, axis_lim)
plt.ylim(0, axis_lim)

# bubble pre
for _, row in df[df.period=='pre'].iterrows():
    x = row[x_alias]
    y = row.y_scaled
    r = row.radius
    circle = patches.Circle((x, y), r, facecolor="#99d8e1", edgecolor="none", linewidth=0, zorder=2)
    plt.gca().add_patch(circle)

# transition space
for consumer in df.consumer.distinctive():
    user_pre = df[(df.user==user) & (df.period=='pre')]
    x1, y1, r1 = user_pre[x_alias].values[0], user_pre.y_scaled.values[0], user_pre.radius.values[0]
    user_post = df[(df.user==user) & (df.period=='post')]
    x2, y2, r2 = user_post[x_alias].values[0], user_post.y_scaled.values[0], user_post.radius.values[0]

    tangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)
    circle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]
    circle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]

    polygon_points = circle_1_line_intersections + circle_2_line_intersections
    draw_polygon(plt, polygon_points)

# bubble put up
for _, row in df[df.period=='post'].iterrows():
    x = row[x_alias]
    y = row.y_scaled
    r = row.radius
    label = row.consumer
    circle = patches.Circle((x, y), r, facecolor="#2d699f", edgecolor="none", linewidth=0, zorder=2)
    plt.gca().add_patch(circle)

    plt.textual content(x, y - r - 0.3, label, fontsize=12, ha="middle")

# bubble measurement legend
legend_areas_original = [150, 50]
legend_position = (11, 10.2)
for i in legend_areas_original:
    i_r = area_to_radius(i) * radius_scaler
    circle = plt.Circle((legend_position[0], legend_position[1] + i_r), i_r, colour="black", fill=False, linewidth=0.6, facecolor="none")
    plt.gca().add_patch(circle)
    plt.textual content(legend_position[0], legend_position[1] + 2*i_r, str(i), fontsize=12, ha="middle", va="middle",
              bbox=dict(facecolor="white", edgecolor="none", boxstyle="spherical,pad=0.1"))
legend_label_r = area_to_radius(np.max(legend_areas_original)) * radius_scaler
plt.textual content(legend_position[0], legend_position[1] + 2*legend_label_r + 0.3, 'Exercise, hours', fontsize=12, ha="middle", va="middle")


## pre-post legend 
# circle 1
legend_position, r1 = (11, 2.2), 0.3
x1, y1 = legend_position[0], legend_position[1]
circle = patches.Circle((x1, y1), r1, facecolor="#99d8e1", edgecolor="none", linewidth=0, zorder=2)
plt.gca().add_patch(circle)
plt.textual content(x1, y1 + r1 + 0.15, 'Pre', fontsize=12, ha="middle", va="middle")
# circle 2
x2, y2 = legend_position[0], legend_position[1] - r1*3
r2 = r1*0.7
circle = patches.Circle((x2, y2), r2, facecolor="#2d699f", edgecolor="none", linewidth=0, zorder=2)
plt.gca().add_patch(circle)
plt.textual content(x2, y2 - r2 - 0.15, 'Submit', fontsize=12, ha="middle", va="middle")
# tangents
tangent_equations = find_tangent_equations(x1, y1, r1, x2, y2, r2)
circle_1_line_intersections = [find_circle_line_intersection(x1, y1, r1, eq[0], eq[1]) for eq in tangent_equations]
circle_2_line_intersections = [find_circle_line_intersection(x2, y2, r2, eq[0], eq[1]) for eq in tangent_equations]
polygon_points = circle_1_line_intersections + circle_2_line_intersections
draw_polygon(plt, polygon_points)
# small arrow
plt.annotate('', xytext=(x1, y1), xy=(x2, y1 - r1*2), arrowprops=dict(edgecolor="black", arrowstyle="->", lw=1))

# y axis formatting
max_y = df[y_alias].max()
nearest_power_of_10 = 10 ** math.ceil(math.log10(max_y))
ticks = [round(nearest_power_of_10/5 * i, 2) for i in range(0, 6)]
yticks_scaled = ticks / df[x_alias].max()
yticklabels = [str(i) for i in ticks]
yticklabels[0] = ''
plt.yticks(yticks_scaled, yticklabels)

plt.savefig("plot_with_white_background.png", bbox_inches="tight", dpi=300)

Including a time dimension to bubble charts enhances their skill to convey dynamic information adjustments intuitively. By implementing easy transitions between “earlier than” and “after” states, customers can higher perceive developments and comparisons over time.

Whereas no ready-made options had been obtainable, growing a customized method proved each difficult and rewarding, requiring mathematical insights and cautious animation strategies. The proposed methodology could be simply prolonged to numerous datasets, making it a helpful device for Knowledge Visualization in enterprise, science, and analytics.


Tags: 4DimensionalBubbleChartsDatatimevisualization

Related Posts

Img 1748 2 scaled 1.jpg
Artificial Intelligence

Learn how to Maximize Technical Occasions — NVIDIA GTC Paris 2025

July 2, 2025
Header 1024x683.png
Artificial Intelligence

Find out how to Entry NASA’s Local weather Information — And How It’s Powering the Struggle Towards Local weather Change Pt. 1

July 2, 2025
Pool 831996 640.jpg
Artificial Intelligence

Prescriptive Modeling Makes Causal Bets – Whether or not You Understand it or Not!

July 1, 2025
Anthony tori 9qykmbbcfjc unsplash scaled 1.jpg
Artificial Intelligence

Classes Realized After 6.5 Years Of Machine Studying

June 30, 2025
Graph 1024x683.png
Artificial Intelligence

Financial Cycle Synchronization with Dynamic Time Warping

June 30, 2025
Pexels jan van der wolf 11680885 12311703 1024x683.jpg
Artificial Intelligence

How you can Unlock the Energy of Multi-Agent Apps

June 29, 2025
Next Post
Humanoids To The Workforce.webp.webp

Humanoids at Work: Revolution or Workforce Takeover?

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

POPULAR NEWS

0 3.png

College endowments be a part of crypto rush, boosting meme cash like Meme Index

February 10, 2025
Gemini 2.0 Fash Vs Gpt 4o.webp.webp

Gemini 2.0 Flash vs GPT 4o: Which is Higher?

January 19, 2025
1da3lz S3h Cujupuolbtvw.png

Scaling Statistics: Incremental Customary Deviation in SQL with dbt | by Yuval Gorchover | Jan, 2025

January 2, 2025
How To Maintain Data Quality In The Supply Chain Feature.jpg

Find out how to Preserve Knowledge High quality within the Provide Chain

September 8, 2024
0khns0 Djocjfzxyr.jpeg

Constructing Data Graphs with LLM Graph Transformer | by Tomaz Bratanic | Nov, 2024

November 5, 2024

EDITOR'S PICK

1o06jxpj Dmbliwnr1p7xwq.png

Kickstart Your Knowledge Science Journey — A Information for Aspiring Knowledge Scientists | by Saankhya Mondal | Nov, 2024

November 7, 2024
Fdf25205 62a4 4ac8 9952 42aaa9ec1e6e 800x420.jpg

Saylor urges Microsoft to ditch bonds, purchase Bitcoin to keep away from destroying capital

May 7, 2025
Openai.jpg

OpenAI will present secret coaching knowledge to copyright legal professionals • The Register

September 26, 2024
1quz70j6kxwf7gf7dykmzcq.png

Lacking Information in Time-Collection? Machine Studying Strategies (Half 2) | by Sara Nóbrega | Jan, 2025

January 9, 2025

About Us

Welcome to News AI World, your go-to source for the latest in artificial intelligence news and developments. Our mission is to deliver comprehensive and insightful coverage of the rapidly evolving AI landscape, keeping you informed about breakthroughs, trends, and the transformative impact of AI technologies across industries.

Categories

  • Artificial Intelligence
  • ChatGPT
  • Crypto Coins
  • Data Science
  • Machine Learning

Recent Posts

  • SWEAT is accessible for buying and selling!
  • From Challenges to Alternatives: The AI-Information Revolution
  • Learn how to Maximize Technical Occasions — NVIDIA GTC Paris 2025
  • Home
  • About Us
  • Contact Us
  • Disclaimer
  • Privacy Policy

© 2024 Newsaiworld.com. All rights reserved.

No Result
View All Result
  • Home
  • Artificial Intelligence
  • ChatGPT
  • Data Science
  • Machine Learning
  • Crypto Coins
  • Contact Us

© 2024 Newsaiworld.com. All rights reserved.

Are you sure want to unlock this post?
Unlock left : 0
Are you sure want to cancel subscription?