Merge branch 'master' into support-react

This commit is contained in:
Tobias Lins 2017-11-04 14:48:11 +01:00 committed by GitHub
commit c6e3e20692
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 6963 additions and 92 deletions

10
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,10 @@
#### Expected Behaviour
#### Actual Behaviour
#### Steps to Reproduce:
*
NOTE: Add a GIF/Screenshot if required.
Frappé Charts version:

View File

@ -1,7 +1,14 @@
<!-- Thank you so much for contributing! We're glad to have you onboard :) -->
<!-- Please help us understand you contribution better with these details -->
###### Explanation About What Code Achieves:
<!-- Please explain why this code is necessary / what it does -->
- Explanation
###### Screenshots/GIFs:
<!-- As this is mainly a visual lib, please include a screenshot/gif if your contribution modifies on-screen components -->
- Screenshot
###### Steps To Test:
<!-- What would someone do to be able to see the effects of your code? -->
- Steps

View File

@ -10,20 +10,8 @@
</div>
<p align="center">
<a href="https://www.npmjs.com/package/frappe-charts">
<img src="https://img.shields.io/npm/v/frappe-charts.svg?maxAge=2592000">
</a>
<a href="https://www.npmjs.com/package/frappe-charts">
<img src="https://img.shields.io/npm/dm/frappe-charts.svg?maxAge=2592000">
</a>
<a href="https://www.npmjs.com/package/frappe-charts">
<img src="https://img.shields.io/npm/dt/frappe-charts.svg?maxAge=2592000">
</a>
<a href="http://github.com/frappe/charts/tree/master/dist/js/frappe-charts.min.js">
<img src="http://img.badgesize.io/frappe/charts/master/dist/frappe-charts.min.js.svg?compression=gzip">
</a>
<a href="https://saythanks.io/to/frappe">
<img src="https://img.shields.io/badge/Say%20Thanks-🦉-1EAEDB.svg?style=flat-square">
<a href="http://github.com/frappe/charts/tree/master/dist/js/frappe-charts.min.iife.js">
<img src="http://img.badgesize.io/frappe/charts/master/dist/frappe-charts.min.iife.js.svg?compression=gzip">
</a>
</p>
@ -47,7 +35,7 @@
* ...or include within your HTML
```html
<script src="https://raw.githubusercontent.com/frappe/charts/master/dist/frappe-charts.min.js"></script>
<script src="https://unpkg.com/frappe-charts@0.0.3/dist/frappe-charts.min.iife.js"></script>
```
#### Usage
@ -74,7 +62,7 @@ const chart = new Chart({
parent: '#chart',
title: "My Awesome Chart",
data: data,
type: 'bar', // or 'line', 'scatter', 'percentage'
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage'
height: 250
})
```

3287
dist/frappe-charts.min.cjs.js vendored Normal file

File diff suppressed because one or more lines are too long

3285
dist/frappe-charts.min.esm.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/frappe-charts.min.iife.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -100,8 +100,10 @@ Array.prototype.slice.call(
let btn = e.target;
let type = btn.getAttribute('data-type');
type_chart = type_chart.get_different_chart(type);
let newChart = type_chart.get_different_chart(type);
if(newChart){
type_chart = newChart;
}
Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
@ -349,25 +351,21 @@ document.querySelector('[data-aggregation="average"]').addEventListener("click",
// Heatmap
// ================================================================================
let heatmap_data = {
1479753000.0: 1,
1498588200.0: 1,
1499193000.0: 1,
1499625000.0: 2,
1500921000.0: 1,
1501612200.0: 1,
1502994600.0: 1,
1503858600.0: 1,
1504809000.0: 3,
1505241000.0: 1,
1506277800.0: 2
};
let heatmap_data = {};
let current_date = new Date();
let timestamp = current_date.getTime()/1000;
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight
for (var i = 0; i< 375; i++) {
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 6);
timestamp = Math.floor(timestamp - 86400).toFixed(1);
}
new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
height: 100,
height: 115,
discrete_domains: 1 // default 0
});
@ -377,24 +375,20 @@ Array.prototype.slice.call(
el.addEventListener('click', (e) => {
let btn = e.target;
let mode = btn.getAttribute('data-mode');
let discrete_domains = 0;
if(mode === 'discrete') {
new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
height: 100,
discrete_domains: 1 // default 0
});
} else {
new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
height: 100
});
discrete_domains = 1;
}
new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
type: 'heatmap',
height: 115,
discrete_domains: discrete_domains
});
Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
@ -426,3 +420,5 @@ function shuffle(array) {
return array;
}

View File

@ -80,7 +80,7 @@
parent: "#chart",
title: "My Awesome Chart",
data: data,
type: 'bar', // or 'line', 'scatter', 'percentage'
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage'
height: 250
});</code></pre>
<div id="chart-types" class="border"></div>
@ -88,6 +88,7 @@
<button type="button" class="btn btn-sm btn-secondary active" data-type='bar'>Bar Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='line'>Line Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='scatter'>Scatter Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button>
</div>
<p class="text-muted">
@ -125,6 +126,18 @@
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button>
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> ...
// Include specific Y values in input data to be displayed as lines
// (before passing data to a new chart):
data.specific_values = [
{
title: "Altitude",
line_type: "dashed", // or "solid"
value: 38
}
]
...</code></pre>
</div>
</div>
@ -215,7 +228,7 @@
parent: "#heatmap",
data: heatmap_data, // object with date/timestamp-value pairs
type: 'heatmap',
height: 100,
height: 115,
discrete_domains: 1 // default 0
});</code></pre>
</div>
@ -226,7 +239,8 @@
<!-- Closing -->
<div class="text-center" style="margin-top: 70px">
<a href="https://github.com/frappe/charts/archive/master.zip"><button class="large blue button">Download</button></a>
<p class="mt-2"><a href="https://github.com/frappe/charts" target="_blank">View on GitHub</a></p>
<p style="margin-top: 3rem;margin-bottom: 1.5rem;"><a href="https://github.com/frappe/charts" target="_blank">View on GitHub</a></p>
<p style="margin-top: 1rem;"><iframe src="https://ghbtns.com/github-btn.html?user=frappe&repo=charts&type=star&count=true" frameborder="0" scrolling="0" width="94px" height="20px"></iframe></p>
<p>License: MIT</p>
</div>
</div>

View File

@ -1,11 +1,16 @@
{
"name": "frappe-charts",
"version": "0.0.1",
"version": "0.0.3",
"description": "https://frappe.github.io/charts",
"main": "src/scripts/charts.js",
"main": "dist/frappe-charts.min.cjs.js",
"module": "dist/frappe-charts.min.esm.js",
"browser": "dist/frappe-charts.min.iife.js",
"directories": {
"doc": "docs"
},
"files":[
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "rollup -c --watch",

View File

@ -10,15 +10,21 @@ import nested from 'postcss-nested';
import cssnext from 'postcss-cssnext';
import cssnano from 'cssnano';
import pkg from './package.json';
export default [
{
input: 'src/scripts/charts.js',
output: {
file: 'dist/frappe-charts.min.js',
format: 'iife',
},
name: 'Chart',
sourcemap: 'true',
output: [
{
file: pkg.main,
format: 'cjs',
},
{
file: pkg.module,
format: 'es',
}
],
plugins: [
postcss({
extensions: [ '.less' ],
@ -40,18 +46,23 @@ export default [
replace({
exclude: 'node_modules/**',
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
}),
uglify()
})
// uglify()
],
},
{
input: 'src/scripts/charts.js',
output: {
file: 'docs/assets/js/frappe-charts.min.js',
format: 'iife',
},
output: [
{
file: 'docs/assets/js/frappe-charts.min.js',
format: 'iife',
},
{
file: pkg.browser,
format: 'iife',
}
],
name: 'Chart',
sourcemap: 'false',
plugins: [
postcss({
extensions: [ '.less' ],

View File

@ -4,6 +4,7 @@ import BarChart from './charts/BarChart';
import LineChart from './charts/LineChart';
import ScatterChart from './charts/ScatterChart';
import PercentageChart from './charts/PercentageChart';
import PieChart from './charts/PieChart';
import Heatmap from './charts/Heatmap';
// if (ENV !== 'production') {
@ -19,7 +20,8 @@ const chartTypes = {
bar: BarChart,
scatter: ScatterChart,
percentage: PercentageChart,
heatmap: Heatmap
heatmap: Heatmap,
pie: PieChart
};
function getChartByType(chartType = 'line', options) {

View File

@ -4,12 +4,10 @@ import Chart from '../charts';
export default class BaseChart {
constructor({
parent = "",
height = 240,
title = '', subtitle = '',
data = {},
format_lambdas = {},
summary = [],
@ -17,7 +15,10 @@ export default class BaseChart {
is_navigable = 0,
has_legend = 0,
type = '' // eslint-disable-line no-unused-vars
type = '', // eslint-disable-line no-unused-vars
parent,
data
}) {
this.raw_chart_args = arguments[0];
@ -37,7 +38,7 @@ export default class BaseChart {
}
this.has_legend = has_legend;
this.chart_types = ['line', 'scatter', 'bar', 'percentage', 'heatmap'];
this.chart_types = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie'];
this.set_margins(height);
}
@ -50,10 +51,11 @@ export default class BaseChart {
// Only across compatible types
let compatible_types = {
bar: ['line', 'scatter', 'percentage'],
line: ['scatter', 'bar', 'percentage'],
scatter: ['line', 'bar', 'percentage'],
percentage: ['bar', 'line', 'scatter'],
bar: ['line', 'scatter', 'percentage', 'pie'],
line: ['scatter', 'bar', 'percentage', 'pie'],
pie: ['line', 'scatter', 'percentage', 'bar'],
scatter: ['line', 'bar', 'percentage', 'pie'],
percentage: ['bar', 'line', 'scatter', 'pie'],
heatmap: []
};
@ -81,6 +83,10 @@ export default class BaseChart {
}
setup() {
if(!this.parent) {
console.error("No parent element to render on was provided.");
return;
}
this.bind_window_events();
this.refresh(true);
}

View File

@ -48,7 +48,7 @@ export default class Heatmap extends BaseChart {
}
set_width() {
this.base_width = (this.no_of_cols) * 12;
this.base_width = (this.no_of_cols + 3) * 12 ;
if(this.discrete_domains) {
this.base_width += (12 * 12);
@ -124,7 +124,8 @@ export default class Heatmap extends BaseChart {
let data_value = 0;
let color_index = 0;
let timestamp = Math.floor(current_date.getTime()/1000).toFixed(1);
let current_timestamp = current_date.getTime()/1000;
let timestamp = Math.floor(current_timestamp - (current_timestamp % 86400)).toFixed(1);
if(this.data[timestamp]) {
data_value = this.data[timestamp];

View File

@ -0,0 +1,220 @@
import BaseChart from './BaseChart';
import $ from '../helpers/dom';
import { lightenDarkenColor } from '../helpers/utils';
const ANGLE_RATIO = Math.PI / 180;
const FULL_ANGLE = 360;
export default class PieChart extends BaseChart {
constructor(args) {
super(args);
this.type = 'pie';
this.get_y_label = this.format_lambdas.y_label;
this.get_x_tooltip = this.format_lambdas.x_tooltip;
this.get_y_tooltip = this.format_lambdas.y_tooltip;
this.elements_to_animate = null;
this.hoverRadio = args.hoverRadio || 0.1;
this.max_slices = 10;
this.max_legend_points = 6;
this.isAnimate = false;
this.colors = args.colors;
this.startAngle = args.startAngle || 0;
this.clockWise = args.clockWise || false;
if(!this.colors || this.colors.length < this.data.labels.length) {
this.colors = ['#7cd6fd', '#5e64ff', '#743ee2', '#ff5858', '#ffa00a',
'#FEEF72', '#28a745', '#98d85b', '#b554ff', '#ffa3ef'];
}
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.setup();
}
setup_values() {
this.centerX = this.width / 2;
this.centerY = this.height / 2;
this.radius = (this.height > this.width ? this.centerX : this.centerY);
this.slice_totals = [];
let all_totals = this.data.labels.map((d, i) => {
let total = 0;
this.data.datasets.map(e => {
total += e.values[i];
});
return [total, d];
}).filter(d => { return d[0] > 0; }); // keep only positive results
let totals = all_totals;
if(all_totals.length > this.max_slices) {
all_totals.sort((a, b) => { return b[0] - a[0]; });
totals = all_totals.slice(0, this.max_slices-1);
let others = all_totals.slice(this.max_slices-1);
let sum_of_others = 0;
others.map(d => {sum_of_others += d[0];});
totals.push([sum_of_others, 'Rest']);
this.colors[this.max_slices-1] = 'grey';
}
this.labels = [];
totals.map(d => {
this.slice_totals.push(d[0]);
this.labels.push(d[1]);
});
this.legend_totals = this.slice_totals.slice(0, this.max_legend_points);
}
setup_utils() { }
static getPositionByAngle(angle,radius){
return {
x:Math.sin(angle * ANGLE_RATIO) * radius,
y:Math.cos(angle * ANGLE_RATIO) * radius,
};
}
makeArcPath(startPosition,endPosition){
const{centerX,centerY,radius,clockWise} = this;
return `M${centerX} ${centerY} L${centerX+startPosition.x} ${centerY+startPosition.y} A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} ${centerX+endPosition.x} ${centerY+endPosition.y} z`;
}
make_graph_components(init){
const{radius,clockWise} = this;
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0);
const prevSlicesProperties = this.slicesProperties || [];
this.slices = [];
this.elements_to_animate = [];
this.slicesProperties = [];
let curAngle = 180 - this.startAngle;
this.slice_totals.map((total, i) => {
const startAngle = curAngle;
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE;
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
const endAngle = curAngle = curAngle + diffAngle;
const startPosition = PieChart.getPositionByAngle(startAngle,radius);
const endPosition = PieChart.getPositionByAngle(endAngle,radius);
const prevProperty = init && prevSlicesProperties[i];
let curStart,curEnd;
if(init){
curStart = prevProperty?prevProperty.startPosition : startPosition;
curEnd = prevProperty? prevProperty.endPosition : startPosition;
}else{
curStart = startPosition;
curEnd = endPosition;
}
const curPath = this.makeArcPath(curStart,curEnd);
let slice = $.createSVG('path',{
inside:this.draw_area,
className:'pie-path',
style:'transition:transform .3s;',
d:curPath,
fill:this.colors[i]
});
this.slices.push(slice);
this.slicesProperties.push({
startPosition,
endPosition,
value:total,
total:this.grand_total,
startAngle,
endAngle,
angle:diffAngle
});
if(init){
this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1},
{d:this.makeArcPath(startPosition,endPosition)},
650, "easein",null,{
d:curPath
}]);
}
});
if(init){
this.run_animation();
}
}
run_animation() {
// if(this.isAnimate) return ;
// this.isAnimate = true;
if(!this.elements_to_animate || this.elements_to_animate.length === 0) return;
let anim_svg = $.runSVGAnimation(this.svg, this.elements_to_animate);
if(this.svg.parentNode == this.chart_wrapper) {
this.chart_wrapper.removeChild(this.svg);
this.chart_wrapper.appendChild(anim_svg);
}
// Replace the new svg (data has long been replaced)
setTimeout(() => {
// this.isAnimate = false;
if(anim_svg.parentNode == this.chart_wrapper) {
this.chart_wrapper.removeChild(anim_svg);
this.chart_wrapper.appendChild(this.svg);
}
}, 650);
}
calTranslateByAngle(property){
const{radius,hoverRadio} = this;
const position = PieChart.getPositionByAngle(property.startAngle+(property.angle / 2),radius);
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
}
hoverSlice(path,i,flag,e){
if(!path) return;
if(flag){
$.transform(path,this.calTranslateByAngle(this.slicesProperties[i]));
path.setAttribute('fill',lightenDarkenColor(this.colors[i],50));
let g_off = $.offset(this.svg);
let x = e.pageX - g_off.left + 10;
let y = e.pageY - g_off.top - 10;
let title = (this.formatted_labels && this.formatted_labels.length>0
? this.formatted_labels[i] : this.labels[i]) + ': ';
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);
this.tip.set_values(x, y, title, percent + "%");
this.tip.show_tip();
}else{
$.transform(path,'translate3d(0,0,0)');
this.tip.hide_tip();
path.setAttribute('fill',this.colors[i]);
}
}
mouseMove(e){
const target = e.target;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
for(let i = 0; i < this.slices.length; i++){
if(target === this.slices[i]){
this.hoverSlice(prevAcitve,prevIndex,false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target,i,true,e);
break;
}
}
}
mouseLeave(){
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
}
bind_tooltip() {
this.draw_area.addEventListener('mousemove',this.mouseMove);
this.draw_area.addEventListener('mouseleave',this.mouseLeave);
}
show_summary() {
let x_values = this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels : this.labels;
this.legend_totals.map((d, i) => {
if(d) {
let stats = $.create('div', {
className: 'stats',
inside: this.stats_wrapper
});
stats.innerHTML = `<span class="indicator">
<i style="background-color:${this.colors[i]};"></i>
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>`;
}
});
}
}

View File

@ -2,6 +2,16 @@ export default function $(expr, con) {
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
}
const EASING = {
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
// easein: "0.42 0 1 1",
easein: "0.1 0.8 0.2 1",
easeout: "0 0 0.58 1",
easeinout: "0.42 0 0.58 1"
};
$.findNodeIndex = (node) =>
{
var i = 0;
@ -83,7 +93,6 @@ $.runSVGAnimation = (svg_container, elements) => {
let anim_element, new_element;
element[0] = obj.unit;
[anim_element, new_element] = $.animateSVG(...element);
new_elements.push(new_element);
@ -108,15 +117,15 @@ $.runSVGAnimation = (svg_container, elements) => {
return anim_svg;
};
$.transform = (element, style)=>{
element.style.transform = style;
element.style.webkitTransform = style;
element.style.msTransform = style;
element.style.mozTransform = style;
element.style.oTransform = style;
};
$.animateSVG = (element, props, dur, easing_type="linear", type=undefined, old_values={}) => {
let easing = {
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
// easein: "0.42 0 1 1",
easein: "0.1 0.8 0.2 1",
easeout: "0 0 0.58 1",
easeinout: "0.42 0 0.58 1"
};
let anim_element = element.cloneNode(true);
let new_element = element.cloneNode(true);
@ -138,7 +147,7 @@ $.animateSVG = (element, props, dur, easing_type="linear", type=undefined, old_v
begin: "0s",
dur: dur/1000 + "s",
values: current_value + ";" + value,
keySplines: easing[easing_type],
keySplines: EASING[easing_type],
keyTimes: "0;1",
calcMode: "spline",
fill: 'freeze'

View File

@ -11,6 +11,25 @@ export function arrays_equal(arr1, arr2) {
return are_equal;
}
function limitColor(r){
if (r > 255) return 255;
else if (r < 0) return 0;
return r;
}
export function lightenDarkenColor(col,amt) {
let usePound = false;
if (col[0] == "#") {
col = col.slice(1);
usePound = true;
}
let num = parseInt(col,16);
let r = limitColor((num >> 16) + amt);
let b = limitColor(((num >> 8) & 0x00FF) + amt);
let g = limitColor((num & 0x0000FF) + amt);
return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
}
export function shuffle(array) {
// https://stackoverflow.com/a/2450976/6495043
// Awesomeness: https://bost.ocks.org/mike/shuffle/

View File

@ -187,6 +187,7 @@
color: #6c7680;
}
.indicator::before,
.indicator i ,
.indicator-right::after {
content: '';
display: inline-block;
@ -194,7 +195,7 @@
width: 8px;
border-radius: 8px;
}
.indicator::before {
.indicator::before,.indicator i {
margin: 0 4px 0 0px;
}
.indicator-right::after {
@ -203,71 +204,85 @@
.background.grey,
.indicator.grey::before,
.indicator.grey i,
.indicator-right.grey::after {
background: #bdd3e6;
}
.background.light-grey,
.indicator.light-grey::before,
.indicator.light-grey i,
.indicator-right.light-grey::after {
background: #F0F4F7;
}
.background.blue,
.indicator.blue::before,
.indicator.blue i,
.indicator-right.blue::after {
background: #5e64ff;
}
.background.red,
.indicator.red::before,
.indicator.red i,
.indicator-right.red::after {
background: #ff5858;
}
.background.green,
.indicator.green::before,
.indicator.green i,
.indicator-right.green::after {
background: #28a745;
}
.background.light-green,
.indicator.light-green::before,
.indicator.light-green i,
.indicator-right.light-green::after {
background: #98d85b;
}
.background.orange,
.indicator.orange::before,
.indicator.orange i,
.indicator-right.orange::after {
background: #ffa00a;
}
.background.violet,
.indicator.violet::before,
.indicator.violet i,
.indicator-right.violet::after {
background: #743ee2;
}
.background.dark-grey,
.indicator.dark-grey::before,
.indicator.dark-grey i,
.indicator-right.dark-grey::after {
background: #b8c2cc;
}
.background.black,
.indicator.black::before,
.indicator.black i,
.indicator-right.black::after {
background: #36414C;
}
.background.yellow,
.indicator.yellow::before,
.indicator.yellow i,
.indicator-right.yellow::after {
background: #FEEF72;
}
.background.light-blue,
.indicator.light-blue::before,
.indicator.light-blue i,
.indicator-right.light-blue::after {
background: #7CD6FD;
}
.background.purple,
.indicator.purple::before,
.indicator.purple i,
.indicator-right.purple::after {
background: #b554ff;
}
.background.magenta,
.indicator.magenta::before,
.indicator.magenta i,
.indicator-right.magenta::after {
background: #ffa3ef;
}