11

У меня приложение на Vue.js и мне нужно прогресс-бар создать. Я использую Svg, потому что другие варианты не подойдут. Как можно разбить SVG-полукруг на равные части. У меня получается сплошная линия. Пытался манипулировать атрибутом stroke-dasharray, но не получается. Необходимо получить такой вид:

прогресс-бар

я пока добился такого

введите сюда описание изображения

мой код:

<div class="radial">
        <svg  xmlns="http://www.w3.org/2000/svg" height="100" width="100" viewBox="0 0 200 200" data-value="40">
            <path class="bg" stroke="#ccc" d="M41 149.5a77 77 0 1 1 117.93 0"  fill="none"/>
            <path class="meter" stroke="#D15F45" d="M41 149.5a77 77 0 1 1 117.93 0" fill="none" pathLength="100" stroke-dasharray="38, 100"/>
        </svg>
    </div>
Alexandr_TT
  • 110,146
  • 23
  • 114
  • 384
Daniyal Lukmanov
  • 533
  • 3
  • 15
  • 1
    Не совсем. Так я тоже делал. Но мне необходимо, чтобы были именно 3 равных сектора. У меня, например, 100% = 30, а непосредственно проценты заполнения будут передаваться переменной. Так вот, если вы в своем примере измените pathlength на 30, то увидите, что результат совсем не такой. – Daniyal Lukmanov Apr 03 '19 at 14:54
  • 1
    @MaximLensky, да. Только как в таком случае передавать проценты заполнения? Он не всегда заполнен на 100% – Daniyal Lukmanov Apr 03 '19 at 14:57
  • В этом и вся суть. Динамически менять не получается - секторы "бегают". Нужно именно в статике эти три сектора всегда иметь неподвижными и неизменяемыми, а заполнять уже с помощью переменной. – Daniyal Lukmanov Apr 03 '19 at 15:02

3 Answers3

13

Самый простой вариант, если фон белый, можно закрыть 2мя белыми линиями, идущими от центра:

requestAnimationFrame(draw)
function draw(dt) {
  document.querySelector('.meter').setAttribute('stroke-dasharray', `${dt/10%100}, 100`);
  requestAnimationFrame(draw)
}
<div class="radial">
  <svg xmlns="http://www.w3.org/2000/svg" height="200" width="200" viewBox="0 0 200 200" data-value="40">
    <path class="bg" stroke-width="22" stroke="#ccc" d="M41 149.5a77 77 0 1 1 117.93 0"  fill="none"/>
    <path class="meter" stroke-width="22" stroke="#D15F45" d="M41 149.5a77 77 0 1 1 117.93 0" fill="none" pathLength="100" stroke-dasharray="38, 100"/>
    <path stroke="white" stroke-width="5" d="M100 100 l-100 -100"/> 
    <path stroke="white" stroke-width="5" d="M100 100 l100 -100"/> 
  </svg>
</div>


Вариант с масками

Тут для бордера используется другая фигура, с большей толщиной линии, от которой маской мы отрезаем ненужное:

<body style="margin:0; overflow:hidden">
    <svg  height="200" width="640">
      <defs>
      <mask id="m">
        <rect width="100%" height="100%" fill="white"/>
        <path stroke="black" stroke-width="2" d="M100 100 l100 -100"/> 
        <path stroke="black" stroke-width="2" d="M100 100 l-100 -100"/> 
        <path fill="black" d="M100 100 l-115 100 l115 100 l115 -100z"/>    
      </mask>
      </defs>
  &lt;g&gt;
    &lt;circle fill="none" stroke-width="25" stroke="#999" r="77" cx="100" cy="100"/&gt;
  &lt;/g&gt;

  &lt;g transform="translate(220,0)"&gt;
    &lt;rect width="100%" height="100%" fill="white"/&gt;
    &lt;path stroke="black" stroke-width="2" d="M100 100 l100 -100"/&gt; 
    &lt;path stroke="black" stroke-width="2" d="M100 100 l-100 -100"/&gt; 
    &lt;path fill="black" d="M100 100 l-115 100 l230 0z"/&gt;    
  &lt;/g&gt;

<g transform="translate(440,0)"> <circle mask="url(#m)"fill="none" stroke-width="25" stroke="#999" r="77" cx="100" cy="100"/> </g> <text text-anchor="middle" alignment-baseline="central" font-size=55 x=210 y=100>+</text> <text text-anchor="middle" alignment-baseline="central" font-size=55 x=420 y=100>=</text> </svg> </body>


Результат:

введите сюда описание изображения

let meter = document.querySelector('path.meter');
let text = document.querySelector('text');
let arrow = document.querySelector('path.arrow');
let length = meter.getTotalLength();

requestAnimationFrame(draw)

function draw(dt) { progress(dt / 100 % 100); requestAnimationFrame(draw) }

function progress(value) { meter.setAttribute('stroke-dasharray', ${length/100*value}, ${length}); arrow.setAttribute('transform', rotate(${-135+value*270/100} 100,100)); text.innerHTML = Math.round(value); }

<svg height="175" viewBox="0 0 200 200">

  <defs>

    <mask id="mask1">
      <rect width="100%" height="100%" fill="white"/>
      <path stroke="black" stroke-width="2" d="M100 100 l100 -100"/> 
      <path stroke="black" stroke-width="2" d="M100 100 l-100 -100"/> 
      <path fill="black" d="M100 100 l-115 100 l230 0z"/>    
    </mask>

    <mask id="mask2">
      <rect width="100%" height="100%" fill="white"/>
      <path stroke="black" stroke-width="5" d="M100 100 l100 -100"/> 
      <path stroke="black" stroke-width="5" d="M100 100 l-100 -100"/>  
    </mask>

   </defs>

   <g fill="none">
     <circle mask="url(#mask1)" stroke-width="25" stroke="#999" r="77" cx="100" cy="100"/>
     <path mask="url(#mask2)" stroke-width="22" stroke="#ccc" d="M41 149.5a77 77 0 1 1 117.93 0"/>
     <path mask="url(#mask2)" stroke-width="22" stroke="#d15f45" d="M41 149.5a77 77 0 1 1 117.93 0" class="meter"/>
     <circle stroke-width="7" stroke="#ccc" r="33" cx="100" cy="100"/>
   </g>
   <text x="100" y="100" font-family="arial" font-size="22px" text-anchor="middle" alignment-baseline="central">0</text>
   <path class="arrow" transform="rotate(-135 100,100)" fill="#ccc" d="M100,65 l5,0 l-5,-10 l-5,10"></path>
 </svg>

UPD: сделал как в вопросе, с циферками и стрелочкой

  • Спасибо. То, что нужно! – Daniyal Lukmanov Apr 03 '19 at 15:14
  • @DaniyalLukmanov рад помочь – Stranger in the Q Apr 03 '19 at 15:15
  • Гэп между секторами надо в d менять, получается? – Daniyal Lukmanov Apr 03 '19 at 15:15
  • @DaniyalLukmanov я сначала загнался с clip, поэтому так, скоро переделаю на толщину, если вы про второй вариант – Stranger in the Q Apr 03 '19 at 15:17
  • Да, я про второй вариант. Кроме того, пытаюсь добавить границы(border) к path class='bg', но тщетно. Мне нужны границы у серого полукруга (задний полукруг). – Daniyal Lukmanov Apr 03 '19 at 15:41
  • @DaniyalLukmanov посмотрите так, однако чтобы сделать обводку надо уде выпендриваться, примерно как в этом сообщении https://ru.stackoverflow.com/questions/962939/Как-сделать-круговую-диаграмму-зависимую-от-чекбоксов/962996#962996 – Stranger in the Q Apr 03 '19 at 15:59
  • Еще раз спасибо. А в stroke-dasharray это для примера было 100. В другом своем SVG я использую, например, 999. Ну вижу вы тоже на 1000 поменяли.)) – Daniyal Lukmanov Apr 03 '19 at 16:13
  • @DaniyalLukmanov но этого недостаточно, длина пути на iphone и desktop разные, я позже изменю второй пример, с телефона не удобно – Stranger in the Q Apr 03 '19 at 16:17
  • @DaniyalLukmanov с обводкой еще наколдовал немного, позже еще подрихтую – Stranger in the Q Apr 03 '19 at 17:09
  • Я пытался плотно разобраться с SVG в целом. Может посоветуете "путь" для получения знаний? Или достаточно документации на MDN? – Daniyal Lukmanov Apr 03 '19 at 17:25
  • @DaniyalLukmanov mdn хорош, но я больше по примерам учусь – Stranger in the Q Apr 03 '19 at 17:34
  • @DaniyalLukmanov запилил – Stranger in the Q Apr 03 '19 at 18:24
  • @Stranger in the Q Чтобы твоя пила не затупилась (+) – Alexandr_TT Apr 03 '19 at 18:47
  • @Alexandr_TT спасибо, в этот раз без d3, но уже с костылями :) из посчитанных фигур было бы легче стилизовать – Stranger in the Q Apr 03 '19 at 19:12
10

Решение с помощью stroke-dasharray

У заданного пути максимальная длина равна 350px
Вычисляется длина с помощью getTotalLength()

 <input  type="button" value="Максимальная длина"  onclick="TotalLength()"/>
 <div>  
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:ev="http://www.w3.org/2001/xml-events"
     width="500" height="500" viewBox="0 0 500 500" >
     &lt;path id="check" fill= "none" stroke ="grey" stroke-width ="1" 

d="M41 149.5a77 77 0 1 1 117.93 0" /> </svg> </div> <script> function TotalLength(){ var path = document.querySelector('#check'); var len = Math.round(path.getTotalLength() ); alert("Длина пути - " + len); }; </script>

Если нужно поделить на три равных сектора, то длина одного сектора будет равна 116.67px
Это сумма длин: черты - 110px и пробела 6.67px

<svg  xmlns="http://www.w3.org/2000/svg" height="200" width="200" viewBox="0 0 200 200" data-value="40">
       <path class="bg" stroke="#ccc" stroke-width="20" stroke-dasharray="110 6.67" 
        d="M41 149.5a77 77 0 1 1 117.93 0"  fill="none"/>
 </svg>         

Более подробно, как делить окружности на равные части с помощью stroke-dasharray здесь и здесь

Анимация заполнения первого сектора основана на изменении значения длины черты от нуля до максимального значения 110px

<animate id="an1" attributeName="stroke-dasharray" begin="0s;an3.end" values="0 110 0 240;110 0 0 240" dur="2s" fill="freeze" />

Точно также заполняются по очереди остальные 2 сектора.

Я сделал сектора разного цвета, при необходимости можно присвоить один цвет.

<svg  xmlns="http://www.w3.org/2000/svg" height="200" width="200" viewBox="0 0 200 200" data-value="40">
       <path class="bg" stroke="#ccc" stroke-width="20" stroke-dasharray="110 6.67" 
        d="M41 149.5a77 77 0 1 1 117.93 0"  fill="none"/>
&lt;path  class="meter" stroke="green" stroke-width="20" 
 d="M41 149.5a77 77 0 1 1 117.93 0"   fill="none"   stroke-dasharray="0 350" stroke-dashoffset="0" &gt; 
 &lt;! анимация заполнения зелёного сектора --&gt;
 &lt;animate id="an1"
      attributeName="stroke-dasharray"
      begin="0s;an3.end"
      values="0 110 0 240;110 0 0 240"
      dur="2s"
      fill="freeze" /&gt;  
&lt;/path&gt;   
               &lt;! анимация заполнения жёлтого сектора --&gt;    
 &lt;path  class="meter" stroke="gold" stroke-width="20" 
   d="M41 149.5a77 77 0 1 1 117.93 0"   fill="none"   stroke-dasharray="0 350" stroke-dashoffset="0" &gt;
 &lt;animate id="an2"
       attributeName="stroke-dasharray"
       begin="an1.end" 
    values="0 116.67  0 110  0 116.67;0 116.67  110 0  0 116.67"
       dur="2s"
       fill="freeze" /&gt;  

</path>
<! анимация заполнения красного сектора --> <path class="meter" stroke="red" stroke-width="20" d="M41 149.5a77 77 0 1 1 117.93 0" fill="none" stroke-dasharray="0 350" stroke-dashoffset="0" > <animate id="an3" attributeName="stroke-dasharray" begin="an2.end" values="0 116.67 0 116.67 0 116.67;0 116.67 0 116.67 110 0" dur="2s" fill="freeze" />
</path> </svg>

Анимация стрелки указателя

<animateTransform id="an_arrow" attributeName="transform" type="rotate" values="-10 100 100;255 100 100" dur="6s" repeatCount="indefinite" fill="freeze" />

<svg  xmlns="http://www.w3.org/2000/svg" height="200" width="200" viewBox="0 0 200 200" data-value="40">
       <path class="bg" stroke="#ccc" stroke-width="20" stroke-dasharray="110 6.67" 
        d="M41 149.5a77 77 0 1 1 117.93 0"  fill="none"/>
&lt;path  class="meter" stroke="green" stroke-width="20" 
 d="M41 149.5a77 77 0 1 1 117.93 0"   fill="none"   stroke-dasharray="0 350" stroke-dashoffset="0" &gt; 
 &lt;! анимация заполнения зелёного сектора --&gt;
 &lt;animate id="an1"
      attributeName="stroke-dasharray"
      begin="0s;an3.end"
      values="0 110 0 240;110 0 0 240"
      dur="2s"
      fill="freeze" /&gt;  
&lt;/path&gt;   
               &lt;! анимация заполнения жёлтого сектора --&gt;    
 &lt;path  class="meter" stroke="gold" stroke-width="20" 
   d="M41 149.5a77 77 0 1 1 117.93 0"   fill="none"   stroke-dasharray="0 350" stroke-dashoffset="0" &gt;
 &lt;animate id="an2"
    attributeName="stroke-dasharray"
    begin="an1.end"
    values="0 116.67  0 110  0 116.67;0 116.67  110 0  0 116.67" dur="2s"
    fill="freeze" /&gt;  

</path>
<! анимация заполнения красного сектора --> <path class="meter" stroke="red" stroke-width="20" d="M41 149.5a77 77 0 1 1 117.93 0" fill="none" stroke-dasharray="0 350" stroke-dashoffset="0" > <animate id="an3" attributeName="stroke-dasharray" begin="an2.end" values="0 116.67 0 116.67 0 116.67;0 116.67 0 116.67 110 0" dur="2s" fill="freeze" />
</path>

<circle cx="100" cy="100" r="50" fill="none" stroke-width="4" stroke="silver" /> <path d="m62.4 134.1c0 2.8 13.6 7.4-2.6 5-2.7-0.4-10.7-0.2-10.7-0.2 0 0 4-13.1 4.1-19.1 0.1-6 9.2 11.5 9.2 14.3z" fill="silver"> <animateTransform id="an_arrow" attributeName="transform" type="rotate" values="-10 100 100;255 100 100" dur="6s" repeatCount="indefinite" fill="freeze" /> </path> <circle cx="100" cy="100" r="3" fill="#8E8E8E" />

&lt;/svg&gt;</code></pre>

Ссылки на связанные топики c заполнением прогрессбара и выводом процентов:

Круговой прогресс бар

Круговой процентный прогресс бар

Круглый векторный индикатор прогресса

Практические примеры применения масок svg

Alexandr_TT
  • 110,146
  • 23
  • 114
  • 384
  • 2
    Все ждал твоего ответа с подробными комментариями....) – Air Apr 03 '19 at 18:58
  • на ios проблемка, похоже надо высчитывать реальную длину пути – Stranger in the Q Apr 03 '19 at 19:14
  • @Stranger in the Q я считал длину пути с помощью TotalLength() Видимо ios не понимает stroke-dasharray c двумя, тремя парами атрибутов – Alexandr_TT Apr 03 '19 at 19:18
  • @Alexandr_TT похоже что на ретине длина отличается – Stranger in the Q Apr 03 '19 at 19:19
  • @Alexandr_TT спасибо за развернутый ответ с пояснениями, но меня это запутало окончательно.)) Не могу понять как остановить анимацию на конкретном значении. У меня, к примеру, 100% = 30, а заполнение я получаю из переменной. Допустим как мне остановить заполнение и стрелку на значении 20 из 30? – Daniyal Lukmanov Apr 04 '19 at 07:15
  • @Daniyal Lukmanov Не за что, был рад дать новую информацию к обучению SVG, но обычно к спасибо добавляют плюсик к ответу :) На словах очень трудно объяснить в комментариях, то что вы хотите понять. Задайте новый вопрос.Почитайте топики, на которые я уже дал ссылки. Сейчас добавлю новые ссылки на топики, где идёт заполнение прогрессбара с выводом процентов заполнения – Alexandr_TT Apr 04 '19 at 07:25
  • @Daniyal Lukmanov Реализация вашего примера затруднена, тем что круг разбит на сектора. Поэтому в первом ответе Stranger in the Q применяет маски для имитации вырезов. Чтобы понять эту технику почитайте топик по использованию масок Я использовал в своем ответе stroke-dasharray Ваша задача сильно упростится, если у вас будет линия без разрывов и тогда можно будет использовать решения из ссылок, которые я привел в ответе. – Alexandr_TT Apr 04 '19 at 08:07
  • 1
    @Alexandr_TT Благодарю. Я немного модифицировал ваш ответ, привязал к переменным необходимые значения - сейчас все, как необходимо работает. )) Учиться многому нужно конечно. Но на то она и профессия - разработчик. ) – Daniyal Lukmanov Apr 04 '19 at 08:14
  • @Daniyal Lukmanov покажите свой ответ, вы вполне можете его опубликовать прямо здесь. Ещё и плюсиков заработаете :) – Alexandr_TT Apr 04 '19 at 08:17
  • 1
    @Alexandr_TT Я сейчас доработаю еще. У меня получается, что я беру основу из ответа Stranger in the Q с добавлением необходимых атрибутов, добавляю туда вашу реализацию стрелки, немного меняя атрибуты анимации. Скоро залью. – Daniyal Lukmanov Apr 04 '19 at 08:20
7

Взяв за основу ответы Stranger in the Q и Alexander_TT, получил итог, который работает как необходимо в условиях моего проекта.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="radial">
        <svg  xmlns="http://www.w3.org/2000/svg" height="200" width="200" viewBox="0 0 200 200" data-value="40">
            <defs>
                <mask id="mask1">
                    <rect width="100%" height="100%" fill="white"/>
                    <path stroke="black" stroke-width="2" d="M100 100 l100 -100"/> 
                    <path stroke="black" stroke-width="2" d="M100 100 l-100 -100"/> 
                    <path fill="black" d="M100 100 l-115 100 l115 100 l115 -100z"/>    
                </mask>
                <mask id="mask2">
                    <rect width="100%" height="100%" fill="white"/>
                    <path stroke="black" stroke-width="5" d="M100 100 l100 -100"/> 
                    <path stroke="black" stroke-width="5" d="M100 100 l-100 -100"/>  
                </mask>
            </defs>
            <circle mask="url(#mask1)" fill="none" stroke-width="25" stroke="#bfb09d" r="77" cx="100" cy="100"/>
            <path mask="url(#mask2)" fill="none" stroke-width="22" stroke="#FFF" d="M41 149.5a77 77 0 1 1 117.93 0"/>
            <path mask="url(#mask2)" fill="none" stroke-width="22" stroke="#D15F45" d="M41 149.5a77 77 0 1 1 117.93 0" class="meter" pathLength="30" stroke-dasharray="22, 999">
                <animate id="an1" attributeName="stroke-dasharray" from="0 0" to="22 999" dur="1s" repeatCount="0" fill="freeze" />
            </path>  
            <circle cx="100" cy="100" r="50" fill="none" stroke-width="4" stroke="#bfb09d" class="arrow-circle"/>
            <path d="m62.4 134.1c0 2.8 13.6 7.4-2.6 5-2.7-0.4-10.7-0.2-10.7-0.2 0 0 4-13.1 4.1-19.1 0.1-6 9.2 11.5 9.2 14.3z" fill="#bfb09d">
                <animateTransform id="an_arrow" attributeName="transform" type="rotate" from="-10 100 100" to="186 100 100" dur="1s" repeatCount="0" fill="freeze" />
            </path>
            <text font-size="36px" x="100" y="100" fill="#D15F45" text-anchor="middle" alignment-baseline="central">22</text>
        </svg>
    </div>

Данные для атрибутов(в примере хардкодом заданы pathLength = 30 и currentVal = 22).

Daniyal Lukmanov
  • 533
  • 3
  • 15
  • 1
    На всякий случай посмотрите мой обновленный ответ =) – Stranger in the Q Apr 04 '19 at 08:40
  • 2
    @StrangerintheQ вы с Alexander_TT не перестаёте удивлять)) – Daniyal Lukmanov Apr 04 '19 at 08:42
  • Однако чтобы сделать закругленные края, как на макете, я бы взял уже вариант с d3.js – Stranger in the Q Apr 04 '19 at 08:47
  • Пока оставлю так. Если вдруг будет необходимость, то уже буду копать дальше. – Daniyal Lukmanov Apr 04 '19 at 08:51
  • @Daniyal Lukmanov поставил плюсик авансом, надеюсь подправите код, чтобы не было сообщений об ошибках – Alexandr_TT Apr 04 '19 at 08:58
  • 2
    @Alexandr_TT ошибки, видимо из-за того, что я просто фрагмент из своего приложения на Vue выложил, чтоб показать откуда данные берутся. Я тогда изменю ответ без этих данных, чтоб просто оставить svg. – Daniyal Lukmanov Apr 04 '19 at 09:03