2023年10月

气泡图是一种多变量的统计图表,可以看作是散点图的变形。
与散点图不同的是,每一个气泡都表示三个维度的数据,除了像散点图一样有X,Y轴,气泡的大小可以表示另一个维度的数据。
例如,x轴表示产品销量,y轴表示产品利润,气泡大小代表产品市场份额百分比。

它可以帮助我们发现变量之间的模式、趋势和异常值。
通过气泡的大小和颜色,我们可以同时比较多个变量的值,并且可以快速识别出具有较大或较小数值的数据点。

1. 主要元素

气泡图通常用于展示和比较数据之间的关系和分布,可以展示三维(X,Y轴,气泡大小),甚至四维数据(X,Y轴,气泡大小,气泡颜色)之间的关系。
它的主要元素包括:

  1. 横轴和纵轴:气泡图通常使用横轴和纵轴来表示两个变量的值。这些变量可以是数值型、分类型或时间型。
  2. 气泡大小:气泡图通过气泡的大小来表示第三个变量的值。通常,气泡的大小与该变量的值成正比,较大的气泡表示较大的数值。
  3. 气泡颜色:气泡图还可以使用颜色来表示第四个变量的值。不同的颜色可以用于区分不同的数据类别或者表示不同的数值范围。

图片来自 antv 官网

2. 适用的场景

气泡图适用的分析场景包括:

  • 多变量关系分析
    :气通过横轴、纵轴和气泡大小,可以同时呈现三个变量的信息,帮助我们发现变量之间的模式、趋势和相关性。
  • 数据聚类和分类
    :气泡颜色可以用于区分不同的数据类别或者表示不同的数值范围。这使得气泡图在数据聚类和分类分析中非常有用,可以帮助我们识别出不同群组或类别之间的差异和相似性。
  • 比较分析
    :用于比较不同类别或不同时间点的数据。通过气泡的大小和颜色,我们可以直观地比较多个变量的值,快速识别出具有较大或较小数值的数据点,从而帮助我们理解数据的分布和变化情况。
  • 异常值检测
    :帮助我们快速识别出具有异常数值的数据点。通过比较气泡的大小和颜色,我们可以发现与其他数据点相比具有明显不同数值的数据,从而帮助我们识别和分析异常情况。

3. 不适用的场景

气泡图在以下情况可能不适用:

  • 大数据集
    :当数据集非常庞大时,气泡图可能不适合展示所有数据点,因为过多的气泡可能会导致图表混乱不清。
  • 单变量分析
    :如果只需要分析单个变量的分布或趋势,气泡图可能过于复杂,不是最佳选择。
  • 离散数据
    :如果数据是离散的,而不是连续的数值型数据,气泡图可能无法有效地展示变量之间的关系。

4. 分析实战

本次使用气泡图分析 2021年
中欧之间的贸易数据
情况。
气泡图可以分析三个维度的对比:

  1. 进口额
    :横轴
  2. 出口额
    :纵轴
  3. 进出口总额
    :气泡大小

4.1. 数据来源

数据来源国家统计局公开的数据,整理好的数据可从下面的地址下载:
https://databook.top/nation/A06

用到的三个统计数据分别是:

  1. 中国同欧洲各国(地区)进出口总额:
    A06050103.csv
  2. 中国向欧洲各国(地区)出口总额:
    A06050203.csv
  3. 中国从欧洲各国(地区)进口总额:
    A06050303.csv
fp = "d:/share/data/A06050103.csv"
df_total = pd.read_csv(fp)

fp = "d:/share/data/A06050203.csv"
df_output = pd.read_csv(fp)

fp = "d:/share/data/A06050303.csv"
df_input = pd.read_csv(fp)

4.2. 数据清理

数据清理步骤主要包括:

  1. 提取每个文件中2021年的数据
  2. 去除中欧整体的交易额数据,只保留和各个国家之间的贸易数据
  3. 合并进出口总额,进口额,出口额到一个数据集中
  4. 过滤多余字符,生成一个表示国家的数据列
#提取每个文件中2021年的数据
df = df_total[df_total["sj"] == 2021]

#去除中欧整体的交易额数据,只保留和各个国家之间的贸易数据
data = df.loc[2:, ["zbCN", "value"]]
#重新映射列的名称
data = data.rename(columns={"zbCN":"country", "value": "total"})

#过滤多余字符,生成一个表示国家的数据列
data["country"] = data["country"].str.replace("中国同", "", regex=False)
data["country"] = data["country"].str.replace("进出口总额(万美元)", "", regex=False)

df = df_input[df_input["sj"] == 2021]
#合并进出口总额,进口额,出口额到一个数据集中
data["input"] = df.loc[2:, ["value"]]

df = df_output[df_output["sj"] == 2021]
#合并进出口总额,进口额,出口额到一个数据集中
data["output"] = df.loc[2:, ["value"]]

data.head(5)

image.png

和欧洲的总体交易数据位于每个数据集的第一行,所用用
loc[2:, ...]
来过滤。

4.3. 分析结果可视化

with plt.style.context("seaborn-v0_8"):
    fig = plt.figure()
    ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])

    ax.scatter(
        data["input"] / 10000,
        data["output"] / 10000,
        data["total"] / 10000,
        c = np.random.rand(len(data)),
        cmap="Accent",
        alpha=0.6,
    )
    ax.set_xlabel("进口额(亿元)")
    ax.set_ylabel("出口额(亿元)")

    x = np.linspace(0, 1400, 7)
    y = x
    ax.plot(x, y, '-')

image.png

从图中可以看出:
横轴是进口额,纵轴是出口额,气泡越大,进出口总额越大。

中间的蓝色线表示进出口额度一样,可以看出,大部分国家都在蓝色线之上,
说明我国和大部分欧洲的贸易都是
顺差

左下角有很多小气泡,说明和大部分国家之间的进出口贸易额不高,也许是欧洲的小国家很多的缘故。

你也许不再需要使用 CSS Media Queries(媒体查询)了

最近,CSS 引入了一项新功能:Container Queries。它可以替代 Media Queries 并实现 Media Queries 无法胜任的任务。

超越 Media Queries 的功能

让我们想象一个场景:在网页上有若干卡片。我们希望在卡片宽度较窄时,卡片内部呈上下布局;而在卡片宽度较宽时,卡片内部呈左右布局。以 Tailwind CSS 为例,可以这样实现:

<html>
  <body class="...">
    <main class="container mx-auto flex flex-wrap">

      <div class="basis-1/2 p-2 @container/card">
        <div class="... flex gap-1 flex-col @sm/card:flex-row">
          <img class="rounded-lg w-full @sm/card:w-48" src="..." alt="">
          <p>Lorem ...</p>
        </div>
      </div>

      <div class="basis-1/2 p-2 @container/card">
        ...
      </div>

      <div class="basis-1/2 p-2 @container/card">
        ...
      </div>

      <div class="basis-1/2 p-2 @container/card">
        ...
      </div>

      <div class="basis-full p-2 @container/card">
        ...
      </div>

    </main>
  </body>
</html>

你可以在 Tailwind CSS Container Queries (
https://github.com/tailwindlabs/tailwindcss-container-queries
) 上找到更多相关信息。

效果如下:

替代 Media Queries

考虑下一个场景:网页上有一个标题栏,我们希望在窄屏时背景色为深红色,在宽屏时为浅红色。以 Tailwind CSS 为例,可以这样实现:

<html class="@container/root">
  <body class="...">
    <h1 class="bg-red-500 @md/root:bg-red-300">
      Header
    </h1>
  </body>
</html>

效果如下:

总结

Container Queries 的引入为我们提供了更灵活的布局和样式控制方式,可以减少对传统的 CSS Media Queries 的需求。这一新特性的使用可以极大地简化响应式设计,为开发者带来更多的便利。在不远的将来,Container Queries 将成为前端开发的标配。

众所周知,Elon Musk 想将 Twitter 重新设计定位成一款“超级应用 - X”的野心已经不再是秘密。伴随着应用商店中 Twitter 标志性的蓝鸟 Logo 被 X 取代后,赛博世界充满了对这件事情各种角度的探讨与分析。

Musk 曾经无数次通过微信这一样本来推广他的“超级 App”愿景,但许多不曾熟悉中国互联网的异国网友,仍然有一连串问题亟待回答。
“微信是啥?”,“超级 App 是啥?”,“超级 App 为什么是所有问题的终极答案?”,“在美国这一套玩法能落地吗?”

与未经挖掘的美国市场不同,中国的移动互联网市场一直是各类互联网巨头的兵家必争之地,但与 Musk 的“打造超级应用”慷慨激昂的愿景相比,国内的互联网市场好像已经默认“超级 App”是任何一个企业想要做大做强的必经之路。

尽管国内市场的各家互联网巨头都打造了自己的“超级 App”并覆盖了过亿的人群,但不同产品之中却又彰显了不同的设计理念与特色,有一些应用看似功能丰富但却并不一定能够带来和谐的使用体验,甚至会有一些产品的体验愈发臃肿。

我们将在下文中尝试对这些设计理念进行一些探讨与分析。

“超级应用”是一个包含万物的生态系统

在 2010 年之前,互联网市场中还并没有出现“超级应用(即 Super App,下文以‘超级应用’指代)”的身影,在那个时候大多数互联网 App 均是以提供垂直领域的功能与服务为主,也就是基于“产品目标用户的使用任务”来梳理 App 的设计思路,并最终深耕 UI 与体验设计。

在那个时候,移动互联网用户大都需要通过下载安装不同的 App 来完成不同场景中的生活服务。社交类的微信和 QQ,电商类的支付宝和手机淘宝,工具类的搜狗输入法和 UC 浏览器,资讯类的今日头条和墨迹天气,影音类的芒果 TV 和网易云音乐。除了这些生活必备类的工具产品,我们还可能会在不同的小众领域有一些自己的心头所好,比如摄影后期的 Snapseed 和 B612,小众领域社交的最右和 lofter 等等。

当我们把时间跳跃到 2020 年,不难发现“超级App”已经进化成一个能覆盖“衣食住行”等各类生活场景的全能应用。
从即时通讯到移动支付,从分享照片到视频直播,“超级应用”已经在 13 亿中国网民的日常生活中扮演了不可或缺的角色

而对于那些前往中国的异国游客而言,踏入中国的需要第一个面对门槛可能就是各种
“超级应用”
。游客们只有在下载安装了“超级 App”之后,才可以通过其完成诸如填写报关单,订餐打车,预定酒店与支付房费等各种出行事宜。而由于这些“超级应用”实际上的“连接一切”的现状,不安装它反而有可能体会到手足无措般的痛苦与无奈。

“超级应用”的“包罗万象” 有点像是它在
日常生活中几乎无处不在和无所不能
。而基于它所创建的这个“包罗万象且不断生长”的生态系统又在潜移默化的影响着无数普通人的日常生活。正如同德国哲学家和媒体理论家彼得·斯洛特迪克曾经描述的那样,“已经吸纳了一切曾经在外部的东西。”


彼得·斯洛特迪克倡议创建一个包含所有生物——人类、动物、植物和机器的“本体论体”

这种“一切性”也给竞争对手留下了
很少的机会
来获得类似的主导地位,互联网巨头将用户在手机与 App 中的每一次点击或滑动都变成了可以获利的了流量入口,这种构建自己的互联网帝国的美好画面,或许正是像马斯克等一众硅谷科技领袖所津津乐道的“护城河”所在。

用户在手机中的每一次点击与滑动都可能为大型的科技公司带来利润与增长空间,在超级应用中“包含一切”的能力给对手也留下了渺小的竞争机会来获得类似的主导地位。

违反直觉的“超级应用”设计哲学

虽然打造自己的“超级应用”来构建互联网帝国的梦想可能正是像 Musk 等硅谷科技领袖所着迷的原因所在。但我却并不认为应该在其中通过修改 ICON 角标,增加闪屏与推送提示,引入信息流等各种“打断用户心流”的方式来进一步吸引用户的注意力。

比如在
这篇文章
中,作者 Jianqing Chen 就这样认为:

微信是一款少用的“具有自己独到设计哲学”的互联网 App,与其他同类产品不同,它不仅很少修改自己的 LOGO 来庆祝节假日或向用户发送通知,也几乎不修改应用开启时的闪屏页吸引用户注意力。在这个略显封闭的应用中,用户只能看到他们的联系人所发布的内容,而非像是微博或抖音通过关注名人来积累关注。


微信的闪屏页在视觉上很干净,十多年来一直没有变化

这种略显缺少“花哨与引人注目”的特性,可能在某种角度反而是刻意为之的产品哲学,正如微信创始人张小龙在 2019 的年度公开演讲中所提到的“微信的设计原则之一是让用户尽快离开它”那样,它尝试通过减少用户在微信中所花费的时间来打造自己的产品特质。

等等,这种产品设计方法与实际的现状看起来有点矛盾,如果说一款“超级应用”试图让用户尽快离开,那它又应该如何维护自己的互联网护城河呢?我们都知道让某款应用受欢迎的做法,通常都是在其中提供各种稀缺性的资源,从而增加用户在应用中花费的时间,从而提升 DAU(即 Daily Active User,日活跃用户),但这里却似乎反其道行之了?

张小龙是这样解释这个问题的,

为了保持用户对应用长期且日常的参与,能够让他们尽快离开反而是重要的”。

也就是说,
对时间和精力较低程度的需要,反而是在这款“超级应用”在产品设计角度中能够保持用户在大多数时候留存与活跃的关键原因

“超级应用”的设计思路与道家的哲学含义不谋而合

张小龙的想法借助于小程序变得逐渐清晰起来。

小程序是这款超级应用中第三方开发的子应用程序集合,能够为用户提供各种轻松访问的功能入口,用户可以通过扫码或者搜索小程序的方式叫出租车、订餐、购买火车票和玩游戏等都不再需要跳出打开第三方 App,这也帮助用户避免了安装或卸载新 App 的繁琐成本。


App 顶部的小程序面板,用户可以从屏幕顶部下拉

在这款超级应用中,小程序被存储在屏幕顶部的一个隐藏面板中,用户可以通过向下滑动屏幕来打开小程序。在这样的设计理念中,小程序几乎消失于所有地方却又无处不在。

很多学者与分析师都曾将这款超级应用称为“生活元素”的存在,它并不明显,也不会打断你正专注的事情,但它又无处不在,就像自然世界中的空气、水等元素一样。

这种无处不在但又并不张扬的环境,与中国古代的道家哲学产生了深刻的共鸣。
道家认为“天下万物生于有,有生于无”,“无”就是构成万物基础的东西。在《道德经》中也说到“道生一(或无),一生二(阴阳),二生三(天、地、人;或阴、阳、气息),三生万物”。
在道家的理念中,是“无”决定了宇宙中一切事物如何产生、如何演化,直至如何消失。

尽管这些古代智者的文本深奥难测,但过去的道家思想有助于人们理解“一切”和“虚无”的相互作用。这种观点为“一切”增添了另一个层面的含义,并为“包含应用”应用的发展开辟了另一种愿景。

也许“超级应用”对“一切”这个词“即是万物又是无物”般的解读,才是他们能够在过去 十多年中成功的秘诀所在。可能许多科技领袖在构思通过打造自己的超级应用来构建护城河时,也可以从对“万物”更深入的理解中收益,而非简单的将“一切”等同于“更多且更大”。

之前,我们通过一系列文章,介绍了如何在Spring Boot中发送邮件:

已经包含了大部分的应用场景。但最近DD在做
YouTube中文配音
的时候,碰到一个问题:

如上图所示,收件人在客户端收到的时候,显示的名称是邮箱的前缀,而不是我们的产品名称,也就是邮箱别名。

开始一直在从Mail的配置类里寻找相关配置项,结果就下面这些内容:

public class MailProperties {

	private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

	/**
	 * SMTP server host. For instance, 'smtp.example.com'.
	 */
	private String host;

	/**
	 * SMTP server port.
	 */
	private Integer port;

	/**
	 * Login user of the SMTP server.
	 */
	private String username;

	/**
	 * Login password of the SMTP server.
	 */
	private String password;

	/**
	 * Protocol used by the SMTP server.
	 */
	private String protocol = "smtp";

	/**
	 * Default MimeMessage encoding.
	 */
	private Charset defaultEncoding = DEFAULT_CHARSET;

可以看到,并没有关于别名的配置项。那么如何设置发件人的别名呢?

最后才发现,原来是在定义发送内容的时候设置的,具体看看下面这个例子:

SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("程序猿DD<xxx@didispace.com>");  // 发件人的别名设置
message.setTo("xxxx@qq.com");
message.setSubject("主题:简单邮件");
message.setText("测试邮件内容");

mailSender.send(message);

如果你是组织比较复杂的邮件,使用
MimeMessage
的话也是一样。在
setFrom
的时候,像上面这样写就可以了。

今日分享就到这里,感谢阅读!如果您学习过程中如遇困难?可以加入我们超高质量的
Spring技术交流群
,参与交流与讨论,更好的学习与进步!更多
Spring Boot教程可以点击直达!
,欢迎收藏与转发支持!

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

本文示例代码已上传至我的
Github
仓库https://github.com/CNFeffery/dash-master


大家好我是费老师,就在昨晚,
Dash
框架发布了其2.14.0新版本,新增的功能中,有一项非常令人兴奋,那就是其针对回调函数这一
Dash
中的核心概念,新增了动态回调函数注册的支持